Merge branch 'master' into master

This commit is contained in:
Damien Elmes 2020-07-31 14:14:10 +10:00 committed by GitHub
commit 0ed50394e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 200 additions and 60 deletions

View File

@ -996,7 +996,7 @@ message GraphsOut {
uint32 next_day_at_secs = 4; uint32 next_day_at_secs = 4;
uint32 scheduler_version = 5; uint32 scheduler_version = 5;
/// Seconds to add to UTC timestamps to get local time. /// Seconds to add to UTC timestamps to get local time.
uint32 local_offset_secs = 7; int32 local_offset_secs = 7;
} }
message RevlogEntry { message RevlogEntry {
@ -1004,7 +1004,7 @@ message RevlogEntry {
LEARNING = 0; LEARNING = 0;
REVIEW = 1; REVIEW = 1;
RELEARNING = 2; RELEARNING = 2;
EARLY_REVIEW = 4; EARLY_REVIEW = 3;
} }
int64 id = 1; int64 id = 1;
int64 cid = 2; int64 cid = 2;

View File

@ -8,7 +8,7 @@ import shutil
import unicodedata import unicodedata
import zipfile import zipfile
from io import BufferedWriter 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 zipfile import ZipFile
from anki import hooks from anki import hooks
@ -20,7 +20,7 @@ from anki.utils import ids2str, namedtmp, splitFields, stripHTML
class Exporter: class Exporter:
includeHTML: Union[bool, None] = None includeHTML: Union[bool, None] = None
ext: Optional[str] = None ext: Optional[str] = None
key: Optional[str] = None key: Union[str, Callable, None] = None
includeTags: Optional[bool] = None includeTags: Optional[bool] = None
includeSched: Optional[bool] = None includeSched: Optional[bool] = None
includeMedia: Optional[bool] = None includeMedia: Optional[bool] = None
@ -92,7 +92,7 @@ class Exporter:
class TextCardExporter(Exporter): class TextCardExporter(Exporter):
key = _("Cards in Plain Text") key = lambda: _("Cards in Plain Text")
ext = ".txt" ext = ".txt"
includeHTML = True includeHTML = True
@ -122,7 +122,7 @@ class TextCardExporter(Exporter):
class TextNoteExporter(Exporter): class TextNoteExporter(Exporter):
key = _("Notes in Plain Text") key = lambda: _("Notes in Plain Text")
ext = ".txt" ext = ".txt"
includeTags = True includeTags = True
includeHTML = True includeHTML = True
@ -164,7 +164,7 @@ where cards.id in %s)"""
class AnkiExporter(Exporter): class AnkiExporter(Exporter):
key = _("Anki 2.0 Deck") key = lambda: _("Anki 2.0 Deck")
ext = ".anki2" ext = ".anki2"
includeSched: Union[bool, None] = False includeSched: Union[bool, None] = False
includeMedia = True includeMedia = True
@ -313,7 +313,7 @@ class AnkiExporter(Exporter):
class AnkiPackageExporter(AnkiExporter): class AnkiPackageExporter(AnkiExporter):
key = _("Anki Deck Package") key = lambda: _("Anki Deck Package")
ext = ".apkg" ext = ".apkg"
def __init__(self, col: Collection) -> None: def __init__(self, col: Collection) -> None:
@ -394,7 +394,7 @@ class AnkiPackageExporter(AnkiExporter):
class AnkiCollectionPackageExporter(AnkiPackageExporter): class AnkiCollectionPackageExporter(AnkiPackageExporter):
key = _("Anki Collection Package") key = lambda: _("Anki Collection Package")
ext = ".colpkg" ext = ".colpkg"
verbatim = True verbatim = True
includeSched = None includeSched = None
@ -426,7 +426,11 @@ class AnkiCollectionPackageExporter(AnkiPackageExporter):
def exporters() -> List[Tuple[str, Any]]: def exporters() -> List[Tuple[str, Any]]:
def id(obj): 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 = [ exps = [
id(AnkiCollectionPackageExporter), id(AnkiCollectionPackageExporter),

View File

@ -179,7 +179,7 @@ class AddonManager:
sys.path.insert(0, self.addonsFolder()) sys.path.insert(0, self.addonsFolder())
# in new code, you may want all_addon_meta() instead # in new code, you may want all_addon_meta() instead
def allAddons(self): def allAddons(self) -> List[str]:
l = [] l = []
for d in os.listdir(self.addonsFolder()): for d in os.listdir(self.addonsFolder()):
path = self.addonsFolder(d) path = self.addonsFolder(d)
@ -188,7 +188,7 @@ class AddonManager:
l.append(d) l.append(d)
l.sort() l.sort()
if os.getenv("ANKIREVADDONS", ""): if os.getenv("ANKIREVADDONS", ""):
l = reversed(l) l = list(reversed(l))
return l return l
def all_addon_meta(self) -> Iterable[AddonMeta]: def all_addon_meta(self) -> Iterable[AddonMeta]:

View File

@ -1655,6 +1655,7 @@ update cards set usn=?, mod=?, did=? where id in """
def _clearUnusedTags(self): def _clearUnusedTags(self):
self.col.tags.registerNotes() self.col.tags.registerNotes()
self.on_tag_list_update()
# Suspending # Suspending
###################################################################### ######################################################################

View File

@ -790,7 +790,7 @@ Enter deck to place new %s cards in, or leave blank:"""
return return
theme_manager.set_night_mode(self.mw.pm.night_mode()) theme_manager.set_night_mode(self.mw.pm.night_mode())
self.mw.reset() self.mw.reset()
tooltip("Changes saved.", parent=self.parent()) tooltip(tr(TR.CARD_TEMPLATES_CHANGES_SAVED), parent=self.parent())
self.cleanup() self.cleanup()
gui_hooks.sidebar_should_refresh_notetypes() gui_hooks.sidebar_should_refresh_notetypes()
return QDialog.accept(self) return QDialog.accept(self)
@ -799,7 +799,7 @@ Enter deck to place new %s cards in, or leave blank:"""
def reject(self) -> None: def reject(self) -> None:
if self.change_tracker.changed(): if self.change_tracker.changed():
if not askUser("Discard changes?"): if not askUser(tr(TR.CARD_TEMPLATES_DISCARD_CHANGES)):
return return
self.cleanup() self.cleanup()
return QDialog.reject(self) return QDialog.reject(self)

View File

@ -85,10 +85,10 @@ class EmptyCardsDialog(QDialog):
self.mw.taskman.run_in_background(delete, on_done) 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 = [] to_delete = []
note: NoteWithEmptyCards
for note in self.report.notes: for note in self.report.notes:
note: NoteWithEmptyCards = note
if keep_notes and note.will_delete_note: if keep_notes and note.will_delete_note:
# leave first card # leave first card
to_delete.extend(note.card_ids[1:]) to_delete.extend(note.card_ids[1:])

View File

@ -2112,6 +2112,84 @@ class _ReviewerWillEndHook:
reviewer_will_end = _ReviewerWillEndHook() 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`.
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."""
_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:
"""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`.
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."""
_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: class _ReviewerWillShowContextMenuHook:
_hooks: List[Callable[["aqt.reviewer.Reviewer", QMenu], None]] = [] _hooks: List[Callable[["aqt.reviewer.Reviewer", QMenu], None]] = []

View File

@ -72,7 +72,7 @@ class LoadMetaResult:
class AnkiRestart(SystemExit): class AnkiRestart(SystemExit):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.exitcode = kwargs.pop("exitcode", 0) self.exitcode = kwargs.pop("exitcode", 0)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs) # type: ignore
class ProfileManager: class ProfileManager:
@ -83,6 +83,7 @@ class ProfileManager:
self.db = None self.db = None
self.profile: Optional[Dict] = None self.profile: Optional[Dict] = None
# instantiate base folder # instantiate base folder
self.base: str
self._setBaseFolder(base) self._setBaseFolder(base)
def setupMeta(self) -> LoadMetaResult: def setupMeta(self) -> LoadMetaResult:
@ -92,7 +93,7 @@ class ProfileManager:
return res return res
# profile load on startup # profile load on startup
def openProfile(self, profile): def openProfile(self, profile) -> None:
if profile: if profile:
if profile not in self.profiles(): if profile not in self.profiles():
QMessageBox.critical(None, "Error", "Requested profile does not exist.") QMessageBox.critical(None, "Error", "Requested profile does not exist.")
@ -105,13 +106,13 @@ class ProfileManager:
# Base creation # Base creation
###################################################################### ######################################################################
def ensureBaseExists(self): def ensureBaseExists(self) -> None:
self._ensureExists(self.base) self._ensureExists(self.base)
# Folder migration # Folder migration
###################################################################### ######################################################################
def _oldFolderLocation(self): def _oldFolderLocation(self) -> str:
if isMac: if isMac:
return os.path.expanduser("~/Documents/Anki") return os.path.expanduser("~/Documents/Anki")
elif isWin: elif isWin:
@ -153,7 +154,7 @@ class ProfileManager:
confirmation = QMessageBox() confirmation = QMessageBox()
confirmation.setIcon(QMessageBox.Warning) confirmation.setIcon(QMessageBox.Warning)
confirmation.setWindowIcon(icon) confirmation.setWindowIcon(icon)
confirmation.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel) confirmation.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel) # type: ignore
confirmation.setWindowTitle(window_title) confirmation.setWindowTitle(window_title)
confirmation.setText( confirmation.setText(
"Anki needs to move its data folder from Documents/Anki to a new location. Proceed?" "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.setWindowTitle(window_title)
progress.setText("Please wait...") progress.setText("Please wait...")
progress.show() progress.show()
app.processEvents() app.processEvents() # type: ignore
shutil.move(oldBase, self.base) shutil.move(oldBase, self.base)
progress.hide() progress.hide()
@ -198,8 +199,8 @@ class ProfileManager:
# Profile load/save # Profile load/save
###################################################################### ######################################################################
def profiles(self): def profiles(self) -> List:
def names(): def names() -> List:
return self.db.list("select name from profiles where name != '_global'") return self.db.list("select name from profiles where name != '_global'")
n = names() n = names()
@ -209,9 +210,9 @@ class ProfileManager:
return n return n
def _unpickle(self, data): def _unpickle(self, data) -> Any:
class Unpickler(pickle.Unpickler): class Unpickler(pickle.Unpickler):
def find_class(self, module, name): def find_class(self, module: str, name: str) -> Any:
if module == "PyQt5.sip": if module == "PyQt5.sip":
try: try:
import PyQt5.sip # pylint: disable=unused-import import PyQt5.sip # pylint: disable=unused-import
@ -234,10 +235,10 @@ class ProfileManager:
up = Unpickler(io.BytesIO(data), errors="ignore") up = Unpickler(io.BytesIO(data), errors="ignore")
return up.load() return up.load()
def _pickle(self, obj): def _pickle(self, obj) -> Any:
return pickle.dumps(obj, protocol=0) return pickle.dumps(obj, protocol=0)
def load(self, name): def load(self, name) -> bool:
assert name != "_global" assert name != "_global"
data = self.db.scalar( data = self.db.scalar(
"select cast(data as blob) from profiles where name = ?", name "select cast(data as blob) from profiles where name = ?", name
@ -261,32 +262,32 @@ details have been forgotten."""
self.save() self.save()
return True return True
def save(self): def save(self) -> None:
sql = "update profiles set data = ? where name = ?" sql = "update profiles set data = ? where name = ?"
self.db.execute(sql, self._pickle(self.profile), self.name) self.db.execute(sql, self._pickle(self.profile), self.name)
self.db.execute(sql, self._pickle(self.meta), "_global") self.db.execute(sql, self._pickle(self.meta), "_global")
self.db.commit() self.db.commit()
def create(self, name): def create(self, name) -> None:
prof = profileConf.copy() prof = profileConf.copy()
self.db.execute( self.db.execute(
"insert or ignore into profiles values (?, ?)", name, self._pickle(prof) "insert or ignore into profiles values (?, ?)", name, self._pickle(prof)
) )
self.db.commit() self.db.commit()
def remove(self, name): def remove(self, name) -> None:
p = self.profileFolder() p = self.profileFolder()
if os.path.exists(p): if os.path.exists(p):
send2trash(p) send2trash(p)
self.db.execute("delete from profiles where name = ?", name) self.db.execute("delete from profiles where name = ?", name)
self.db.commit() self.db.commit()
def trashCollection(self): def trashCollection(self) -> None:
p = self.collectionPath() p = self.collectionPath()
if os.path.exists(p): if os.path.exists(p):
send2trash(p) send2trash(p)
def rename(self, name): def rename(self, name) -> None:
oldName = self.name oldName = self.name
oldFolder = self.profileFolder() oldFolder = self.profileFolder()
self.name = name self.name = name
@ -337,19 +338,19 @@ and no other programs are accessing your profile folders, then try again."""
# Folder handling # Folder handling
###################################################################### ######################################################################
def profileFolder(self, create=True): def profileFolder(self, create=True) -> str:
path = os.path.join(self.base, self.name) path = os.path.join(self.base, self.name)
if create: if create:
self._ensureExists(path) self._ensureExists(path)
return path return path
def addonFolder(self): def addonFolder(self) -> str:
return self._ensureExists(os.path.join(self.base, "addons21")) 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")) return self._ensureExists(os.path.join(self.profileFolder(), "backups"))
def collectionPath(self): def collectionPath(self) -> str:
return os.path.join(self.profileFolder(), "collection.anki2") return os.path.join(self.profileFolder(), "collection.anki2")
# Downgrade # Downgrade
@ -377,12 +378,12 @@ and no other programs are accessing your profile folders, then try again."""
# Helpers # Helpers
###################################################################### ######################################################################
def _ensureExists(self, path): def _ensureExists(self, path: str) -> str:
if not os.path.exists(path): if not os.path.exists(path):
os.makedirs(path) os.makedirs(path)
return path return path
def _setBaseFolder(self, cmdlineBase): def _setBaseFolder(self, cmdlineBase: None) -> None:
if cmdlineBase: if cmdlineBase:
self.base = os.path.abspath(cmdlineBase) self.base = os.path.abspath(cmdlineBase)
elif os.environ.get("ANKI_BASE"): 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.maybeMigrateFolder()
self.ensureBaseExists() self.ensureBaseExists()
def _defaultBase(self): def _defaultBase(self) -> str:
if isWin: if isWin:
from aqt.winpaths import get_appdata 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) result.firstTime = not os.path.exists(path)
def recover(): def recover() -> None:
# if we can't load profile, start with a new one # if we can't load profile, start with a new one
if self.db: if self.db:
try: try:
@ -471,7 +472,7 @@ create table if not exists profiles
) )
return result return result
def _ensureProfile(self): def _ensureProfile(self) -> None:
"Create a new profile if none exists." "Create a new profile if none exists."
self.create(_("User 1")) self.create(_("User 1"))
p = os.path.join(self.base, "README.txt") 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 # On first run, allow the user to choose the default language
def setDefaultLang(self): def setDefaultLang(self) -> None:
# create dialog # create dialog
class NoCloseDiag(QDialog): class NoCloseDiag(QDialog):
def reject(self): def reject(self):
@ -519,20 +520,20 @@ create table if not exists profiles
f.lang.setCurrentRow(idx) f.lang.setCurrentRow(idx)
d.exec_() d.exec_()
def _onLangSelected(self): def _onLangSelected(self) -> None:
f = self.langForm f = self.langForm
obj = anki.lang.langs[f.lang.currentRow()] obj = anki.lang.langs[f.lang.currentRow()]
code = obj[1] code = obj[1]
name = obj[0] name = obj[0]
en = "Are you sure you wish to display Anki's interface in %s?" en = "Are you sure you wish to display Anki's interface in %s?"
r = QMessageBox.question( 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: if r != QMessageBox.Yes:
return self.setDefaultLang() return self.setDefaultLang()
self.setLang(code) self.setLang(code)
def setLang(self, code): def setLang(self, code) -> None:
self.meta["defaultLang"] = code self.meta["defaultLang"] = code
sql = "update profiles set data = ? where name = ?" sql = "update profiles set data = ? where name = ?"
self.db.execute(sql, self._pickle(self.meta), "_global") self.db.execute(sql, self._pickle(self.meta), "_global")
@ -542,10 +543,10 @@ create table if not exists profiles
# OpenGL # OpenGL
###################################################################### ######################################################################
def _glPath(self): def _glPath(self) -> str:
return os.path.join(self.base, "gldriver") return os.path.join(self.base, "gldriver")
def glMode(self): def glMode(self) -> str:
if isMac: if isMac:
return "auto" return "auto"
@ -562,11 +563,11 @@ create table if not exists profiles
return mode return mode
return "auto" return "auto"
def setGlMode(self, mode): def setGlMode(self, mode) -> None:
with open(self._glPath(), "w") as file: with open(self._glPath(), "w") as file:
file.write(mode) file.write(mode)
def nextGlMode(self): def nextGlMode(self) -> None:
mode = self.glMode() mode = self.glMode()
if mode == "software": if mode == "software":
self.setGlMode("auto") self.setGlMode("auto")
@ -591,7 +592,7 @@ create table if not exists profiles
def last_addon_update_check(self) -> int: def last_addon_update_check(self) -> int:
return self.meta.get("last_addon_update_check", 0) 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 self.meta["last_addon_update_check"] = secs
def night_mode(self) -> bool: def night_mode(self) -> bool:
@ -642,7 +643,7 @@ create table if not exists profiles
def auto_sync_media_minutes(self) -> int: def auto_sync_media_minutes(self) -> int:
return self.profile.get("autoSyncMediaMinutes", 15) 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 self.profile["autoSyncMediaMinutes"] = val
###################################################################### ######################################################################

View File

@ -13,6 +13,7 @@ from PyQt5.Qt import * # type: ignore
from PyQt5.QtCore import * from PyQt5.QtCore import *
from PyQt5.QtCore import pyqtRemoveInputHook # pylint: disable=no-name-in-module from PyQt5.QtCore import pyqtRemoveInputHook # pylint: disable=no-name-in-module
from PyQt5.QtGui import * # type: ignore from PyQt5.QtGui import * # type: ignore
from PyQt5.QtWebChannel import QWebChannel
from PyQt5.QtWebEngineWidgets import * from PyQt5.QtWebEngineWidgets import *
from PyQt5.QtWidgets import * from PyQt5.QtWidgets import *

View File

@ -183,10 +183,14 @@ class Reviewer:
q = c.q() q = c.q()
# play audio? # play audio?
if c.autoplay(): 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: else:
av_player.clear_queue_and_maybe_interrupt() 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 # render & update bottom
q = self._mungeQA(q) q = self._mungeQA(q)
q = gui_hooks.card_will_show(q, c, "reviewQuestion") q = gui_hooks.card_will_show(q, c, "reviewQuestion")
@ -225,10 +229,14 @@ class Reviewer:
a = c.a() a = c.a()
# play audio? # play audio?
if c.autoplay(): 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: else:
av_player.clear_queue_and_maybe_interrupt() 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 = self._mungeQA(a)
a = gui_hooks.card_will_show(a, c, "reviewAnswer") a = gui_hooks.card_will_show(a, c, "reviewAnswer")
# render and update bottom # render and update bottom

View File

@ -30,9 +30,9 @@ class AnkiWebPage(QWebEnginePage):
self._setupBridge() self._setupBridge()
self.open_links_externally = True self.open_links_externally = True
def _setupBridge(self): def _setupBridge(self) -> None:
class Bridge(QObject): class Bridge(QObject):
@pyqtSlot(str, result=str) @pyqtSlot(str, result=str) # type: ignore
def cmd(self, str): def cmd(self, str):
return json.dumps(self.onCmd(str)) return json.dumps(self.onCmd(str))

View File

@ -88,3 +88,11 @@ check_untyped_defs=true
check_untyped_defs=true check_untyped_defs=true
[mypy-aqt.modelchooser] [mypy-aqt.modelchooser]
check_untyped_defs=true check_untyped_defs=true
[mypy-aqt.webview]
check_untyped_defs=true
[mypy-aqt.addons]
check_untyped_defs=true
[mypy-aqt.emptycards]
check_untyped_defs=true
[mypy-aqt.profiles]
check_untyped_defs=true

View File

@ -90,6 +90,36 @@ hooks = [
legacy_hook="reviewCleanup", legacy_hook="reviewCleanup",
doc="Called before Anki transitions from the review screen to another screen.", 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]"],
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`.
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.""",
),
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`.
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.""",
),
# Debug # Debug
################### ###################
Hook( Hook(

View File

@ -13,7 +13,9 @@ card-templates-preview-box = Preview
card-templates-template-box = Template card-templates-template-box = Template
card-templates-sample-cloze = This is a {"{{c1::"}sample{"}}"} cloze deletion. card-templates-sample-cloze = This is a {"{{c1::"}sample{"}}"} cloze deletion.
card-templates-fill-empty = Fill Empty Fields card-templates-fill-empty = Fill Empty Fields
card-templates-invalid-template-number = Please correct the problems on card template { $number } first.
card-templates-invert-night-mode = Invert Night Mode card-templates-invert-night-mode = Invert Night Mode
card-templates-add-mobile-class = Add mobile Class card-templates-add-mobile-class = Add mobile Class
card-templates-preview-settings = Preview Settings card-templates-preview-settings = Preview Settings
card-templates-invalid-template-number = Card template { $number } has a problem.
card-templates-changes-saved = Changes saved.
card-templates-discard-changes = Discard changes?

View File

@ -41,7 +41,7 @@ impl Collection {
days_elapsed: timing.days_elapsed, days_elapsed: timing.days_elapsed,
next_day_at_secs: timing.next_day_at as u32, next_day_at_secs: timing.next_day_at as u32,
scheduler_version: self.sched_ver() 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,
}) })
} }
} }

View File

@ -36,6 +36,7 @@ body {
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
max-width: 80em; max-width: 80em;
page-break-inside: avoid;
} }
.graph h1 { .graph h1 {
@ -67,6 +68,12 @@ body {
padding: 0.5em; padding: 0.5em;
} }
@media print {
.range-box {
position: absolute;
}
}
.range-box-pad { .range-box-pad {
height: 4em; height: 4em;
} }