Merge branch 'master' into master
This commit is contained in:
commit
0ed50394e7
@ -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;
|
||||||
|
@ -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),
|
||||||
|
@ -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]:
|
||||||
|
@ -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
|
||||||
######################################################################
|
######################################################################
|
||||||
|
@ -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)
|
||||||
|
@ -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:])
|
||||||
|
@ -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]] = []
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
|
@ -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 *
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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))
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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(
|
||||||
|
@ -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?
|
||||||
|
@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user