expand backend Preferences and make undoable

- moved 'default to current deck when adding' into prefs
- move some profile options into the collection config, so they're
undoable and will sync. There is (currently) no automatic migration
from the old profile settings, meaning users will need to set the
options again if they've customized them.
- tidy up preferences.py
- drop the deleteMedia option that was not exposed in the UI
This commit is contained in:
Damien Elmes 2021-03-10 18:20:37 +10:00
parent 24ad7c1f35
commit 6b1dd9ee19
11 changed files with 362 additions and 259 deletions

View File

@ -20,7 +20,7 @@ from bs4 import BeautifulSoup
import aqt
import aqt.sound
from anki.cards import Card
from anki.collection import SearchNode
from anki.collection import Config, SearchNode
from anki.consts import MODEL_CLOZE
from anki.hooks import runFilter
from anki.httpclient import HttpClient
@ -781,7 +781,7 @@ class Editor:
filter = f"{tr(TR.EDITING_MEDIA)} ({extension_filter})"
def accept(file: str) -> None:
self.addMedia(file, canDelete=True)
self.addMedia(file)
file = getFile(
parent=self.widget,
@ -793,24 +793,18 @@ class Editor:
self.parentWindow.activateWindow()
def addMedia(self, path: str, canDelete: bool = False) -> None:
"""canDelete is a legacy arg and is ignored."""
try:
html = self._addMedia(path, canDelete)
html = self._addMedia(path)
except Exception as e:
showWarning(str(e))
return
self.web.eval(f"setFormat('inserthtml', {json.dumps(html)});")
def _addMedia(self, path: str, canDelete: bool = False) -> str:
"Add to media folder and return local img or sound tag."
"""Add to media folder and return local img or sound tag."""
# copy to media folder
fname = self.mw.col.media.addFile(path)
# remove original?
if canDelete and self.mw.pm.profile["deleteMedia"]:
if os.path.abspath(fname) != os.path.abspath(path):
try:
os.unlink(path)
except:
pass
# return a local html link
return self.fnameToLink(fname)
@ -1091,7 +1085,6 @@ class EditorWebView(AnkiWebView):
def __init__(self, parent: QWidget, editor: Editor) -> None:
AnkiWebView.__init__(self, title="editor")
self.editor = editor
self.strip = self.editor.mw.pm.profile["stripHTML"]
self.setAcceptDrops(True)
self._markInternal = False
clip = self.editor.mw.app.clipboard()
@ -1110,10 +1103,12 @@ class EditorWebView(AnkiWebView):
self.triggerPageAction(QWebEnginePage.Copy)
def _wantsExtendedPaste(self) -> bool:
extended = not (self.editor.mw.app.queryKeyboardModifiers() & Qt.ShiftModifier)
if self.editor.mw.pm.profile.get("pasteInvert", False):
extended = not extended
return extended
strip_html = self.editor.mw.col.get_config_bool(
Config.Bool.PASTE_STRIPS_FORMATTING
)
if self.editor.mw.app.queryKeyboardModifiers() & Qt.ShiftModifier:
strip_html = not strip_html
return strip_html
def _onPaste(self, mode: QClipboard.Mode) -> None:
extended = self._wantsExtendedPaste()
@ -1240,7 +1235,7 @@ class EditorWebView(AnkiWebView):
return None
im = QImage(mime.imageData())
uname = namedtmp("paste")
if self.editor.mw.pm.profile.get("pastePNG", False):
if self.editor.mw.col.get_config_bool(Config.Bool.PASTE_IMAGES_AS_PNG):
ext = ".png"
im.save(uname + ext, None, 50)
else:

View File

@ -80,7 +80,7 @@
</widget>
</item>
<item>
<widget class="QCheckBox" name="pasteInvert">
<widget class="QCheckBox" name="paste_strips_formatting">
<property name="text">
<string>PREFERENCES_PASTE_WITHOUT_SHIFT_KEY_STRIPS_FORMATTING</string>
</property>
@ -589,7 +589,7 @@
<tabstop>showPlayButtons</tabstop>
<tabstop>interrupt_audio</tabstop>
<tabstop>pastePNG</tabstop>
<tabstop>pasteInvert</tabstop>
<tabstop>paste_strips_formatting</tabstop>
<tabstop>nightMode</tabstop>
<tabstop>useCurrent</tabstop>
<tabstop>recording_driver</tabstop>

View File

@ -27,7 +27,7 @@ import aqt.toolbar
import aqt.webview
from anki import hooks
from anki._backend import RustBackend as _RustBackend
from anki.collection import BackendUndo, Checkpoint, Collection, ReviewUndo
from anki.collection import BackendUndo, Checkpoint, Collection, Config, ReviewUndo
from anki.decks import Deck
from anki.hooks import runHook
from anki.sound import AVTag, SoundOrVideoTag
@ -391,8 +391,6 @@ class AnkiQt(QMainWindow):
if not self.loadCollection():
return
self.pm.apply_profile_options()
# show main window
if self.pm.profile["mainWindowState"]:
restoreGeom(self, "mainWindow")
@ -467,10 +465,10 @@ class AnkiQt(QMainWindow):
def _add_play_buttons(self, text: str) -> str:
"Return card text with play buttons added, or stripped."
if self.pm.profile.get("showPlayButtons", True):
return aqt.sound.av_refs_to_play_icons(text)
else:
if self.col.get_config_bool(Config.Bool.HIDE_AUDIO_PLAY_BUTTONS):
return anki.sound.strip_av_refs(text)
else:
return aqt.sound.av_refs_to_play_icons(text)
def prepare_card_text_for_display(self, text: str) -> str:
text = self.col.media.escape_media_filenames(text)
@ -508,6 +506,7 @@ class AnkiQt(QMainWindow):
try:
self.update_undo_actions()
gui_hooks.collection_did_load(self.col)
self.apply_collection_options()
self.moveToState("deckBrowser")
except Exception as e:
# dump error to stderr so it gets picked up by errors.py
@ -572,6 +571,12 @@ class AnkiQt(QMainWindow):
self.col.reopen(after_full_sync=False)
self.col.close_for_full_sync()
def apply_collection_options(self) -> None:
"Setup audio after collection loaded."
aqt.sound.av_player.interrupt_current_audio = self.col.get_config_bool(
Config.Bool.INTERRUPT_AUDIO_WHEN_ANSWERING
)
# Backup and auto-optimize
##########################################################################

View File

@ -1,7 +1,9 @@
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import anki.lang
import aqt
from anki.consts import newCardSchedulingLabels
from aqt import AnkiQt
from aqt.profiles import RecordingDriver, VideoDriver
from aqt.qt import *
@ -16,21 +18,6 @@ from aqt.utils import (
)
def video_driver_name_for_platform(driver: VideoDriver) -> str:
if driver == VideoDriver.ANGLE:
return tr(TR.PREFERENCES_VIDEO_DRIVER_ANGLE)
elif driver == VideoDriver.Software:
if isMac:
return tr(TR.PREFERENCES_VIDEO_DRIVER_SOFTWARE_MAC)
else:
return tr(TR.PREFERENCES_VIDEO_DRIVER_SOFTWARE_OTHER)
else:
if isMac:
return tr(TR.PREFERENCES_VIDEO_DRIVER_OPENGL_MAC)
else:
return tr(TR.PREFERENCES_VIDEO_DRIVER_OPENGL_OTHER)
class Preferences(QDialog):
def __init__(self, mw: AnkiQt) -> None:
QDialog.__init__(self, mw, Qt.Window)
@ -45,22 +32,18 @@ class Preferences(QDialog):
self.form.buttonBox.helpRequested, lambda: openHelp(HelpPage.PREFERENCES)
)
self.silentlyClose = True
self.prefs = self.mw.col.get_preferences()
self.setupLang()
self.setupCollection()
self.setupNetwork()
self.setupBackup()
self.setupOptions()
self.setup_collection()
self.setup_profile()
self.setup_global()
self.show()
def accept(self) -> None:
# avoid exception if main window is already closed
if not self.mw.col:
return
self.updateCollection()
self.updateNetwork()
self.updateBackup()
self.updateOptions()
self.update_collection()
self.update_profile()
self.update_global()
self.mw.pm.save()
self.mw.reset()
self.done(0)
@ -69,16 +52,214 @@ class Preferences(QDialog):
def reject(self) -> None:
self.accept()
# Language
# Preferences stored in the collection
######################################################################
def setupLang(self) -> None:
def setup_collection(self) -> None:
self.prefs = self.mw.col.get_preferences()
form = self.form
scheduling = self.prefs.scheduling
form.lrnCutoff.setValue(int(scheduling.learn_ahead_secs / 60.0))
form.newSpread.addItems(list(newCardSchedulingLabels(self.mw.col).values()))
form.newSpread.setCurrentIndex(scheduling.new_review_mix)
form.dayLearnFirst.setChecked(scheduling.day_learn_first)
form.dayOffset.setValue(scheduling.rollover)
if scheduling.scheduler_version < 2:
form.dayLearnFirst.setVisible(False)
form.legacy_timezone.setVisible(False)
else:
form.legacy_timezone.setChecked(not scheduling.new_timezone)
reviewing = self.prefs.reviewing
form.timeLimit.setValue(int(reviewing.time_limit_secs / 60.0))
form.showEstimates.setChecked(reviewing.show_intervals_on_buttons)
form.showProgress.setChecked(reviewing.show_remaining_due_counts)
form.showPlayButtons.setChecked(not reviewing.hide_audio_play_buttons)
form.interrupt_audio.setChecked(reviewing.interrupt_audio_when_answering)
editing = self.prefs.editing
form.useCurrent.setCurrentIndex(
0 if editing.adding_defaults_to_current_deck else 1
)
form.paste_strips_formatting.setChecked(editing.paste_strips_formatting)
form.pastePNG.setChecked(editing.paste_images_as_png)
def update_collection(self) -> None:
form = self.form
scheduling = self.prefs.scheduling
scheduling.new_review_mix = form.newSpread.currentIndex()
scheduling.learn_ahead_secs = form.lrnCutoff.value() * 60
scheduling.day_learn_first = form.dayLearnFirst.isChecked()
scheduling.rollover = form.dayOffset.value()
scheduling.new_timezone = not form.legacy_timezone.isChecked()
reviewing = self.prefs.reviewing
reviewing.show_remaining_due_counts = form.showProgress.isChecked()
reviewing.show_intervals_on_buttons = form.showEstimates.isChecked()
reviewing.time_limit_secs = form.timeLimit.value() * 60
reviewing.hide_audio_play_buttons = not self.form.showPlayButtons.isChecked()
reviewing.interrupt_audio_when_answering = self.form.interrupt_audio.isChecked()
editing = self.prefs.editing
editing.adding_defaults_to_current_deck = not form.useCurrent.currentIndex()
editing.paste_images_as_png = self.form.pastePNG.isChecked()
editing.paste_strips_formatting = self.form.paste_strips_formatting.isChecked()
self.mw.col.set_preferences(self.prefs)
self.mw.apply_collection_options()
# Preferences stored in the profile
######################################################################
def setup_profile(self) -> None:
"Setup options stored in the user profile."
self.setup_recording_driver()
self.setup_network()
self.setup_backup()
def update_profile(self) -> None:
self.update_recording_driver()
self.update_network()
self.update_backup()
# Profile: recording driver
######################################################################
def setup_recording_driver(self) -> None:
self._recording_drivers = [
RecordingDriver.QtAudioInput,
RecordingDriver.PyAudio,
]
# The plan is to phase out PyAudio soon, so will hold off on
# making this string translatable for now.
self.form.recording_driver.addItems(
[
f"Voice recording driver: {driver.value}"
for driver in self._recording_drivers
]
)
self.form.recording_driver.setCurrentIndex(
self._recording_drivers.index(self.mw.pm.recording_driver())
)
def update_recording_driver(self) -> None:
new_audio_driver = self._recording_drivers[
self.form.recording_driver.currentIndex()
]
if self.mw.pm.recording_driver() != new_audio_driver:
self.mw.pm.set_recording_driver(new_audio_driver)
if new_audio_driver == RecordingDriver.PyAudio:
showInfo(
"""\
The PyAudio driver will likely be removed in a future update. If you find it works better \
for you than the default driver, please let us know on the Anki forums."""
)
# Profile: network
######################################################################
def setup_network(self) -> None:
self.form.media_log.setText(tr(TR.SYNC_MEDIA_LOG_BUTTON))
qconnect(self.form.media_log.clicked, self.on_media_log)
self.form.syncOnProgramOpen.setChecked(self.prof["autoSync"])
self.form.syncMedia.setChecked(self.prof["syncMedia"])
self.form.autoSyncMedia.setChecked(self.mw.pm.auto_sync_media_minutes() != 0)
if not self.prof["syncKey"]:
self._hide_sync_auth_settings()
else:
self.form.syncUser.setText(self.prof.get("syncUser", ""))
qconnect(self.form.syncDeauth.clicked, self.sync_logout)
self.form.syncDeauth.setText(tr(TR.SYNC_LOG_OUT_BUTTON))
def on_media_log(self) -> None:
self.mw.media_syncer.show_sync_log()
def _hide_sync_auth_settings(self) -> None:
self.form.syncDeauth.setVisible(False)
self.form.syncUser.setText("")
self.form.syncLabel.setText(
tr(TR.PREFERENCES_SYNCHRONIZATIONNOT_CURRENTLY_ENABLED_CLICK_THE_SYNC)
)
def sync_logout(self) -> None:
if self.mw.media_syncer.is_syncing():
showWarning("Can't log out while sync in progress.")
return
self.prof["syncKey"] = None
self.mw.col.media.force_resync()
self._hide_sync_auth_settings()
def update_network(self) -> None:
self.prof["autoSync"] = self.form.syncOnProgramOpen.isChecked()
self.prof["syncMedia"] = self.form.syncMedia.isChecked()
self.mw.pm.set_auto_sync_media_minutes(
self.form.autoSyncMedia.isChecked() and 15 or 0
)
if self.form.fullSync.isChecked():
self.mw.col.modSchema(check=False)
# Profile: backup
######################################################################
def setup_backup(self) -> None:
self.form.numBackups.setValue(self.prof["numBackups"])
def update_backup(self) -> None:
self.prof["numBackups"] = self.form.numBackups.value()
# Global preferences
######################################################################
def setup_global(self) -> None:
"Setup options global to all profiles."
self.form.uiScale.setValue(int(self.mw.pm.uiScale() * 100))
self.form.nightMode.setChecked(self.mw.pm.night_mode())
self.setup_language()
self.setup_video_driver()
self.setupOptions()
def update_global(self) -> None:
restart_required = False
self.update_video_driver()
newScale = self.form.uiScale.value() / 100
if newScale != self.mw.pm.uiScale():
self.mw.pm.setUiScale(newScale)
restart_required = True
if self.mw.pm.night_mode() != self.form.nightMode.isChecked():
self.mw.pm.set_night_mode(not self.mw.pm.night_mode())
restart_required = True
if restart_required:
showInfo(tr(TR.PREFERENCES_CHANGES_WILL_TAKE_EFFECT_WHEN_YOU))
self.updateOptions()
# legacy - one of Henrik's add-ons is currently wrapping them
def setupOptions(self) -> None:
pass
def updateOptions(self) -> None:
pass
# Global: language
######################################################################
def setup_language(self) -> None:
f = self.form
f.lang.addItems([x[0] for x in anki.lang.langs])
f.lang.setCurrentIndex(self.langIdx())
qconnect(f.lang.currentIndexChanged, self.onLangIdxChanged)
f.lang.setCurrentIndex(self.current_lang_index())
qconnect(f.lang.currentIndexChanged, self.on_language_index_changed)
def langIdx(self) -> int:
def current_lang_index(self) -> int:
codes = [x[1] for x in anki.lang.langs]
lang = anki.lang.currentLang
if lang in anki.lang.compatMap:
@ -90,43 +271,16 @@ class Preferences(QDialog):
except:
return codes.index("en_US")
def onLangIdxChanged(self, idx: int) -> None:
def on_language_index_changed(self, idx: int) -> None:
code = anki.lang.langs[idx][1]
self.mw.pm.setLang(code)
showInfo(
tr(TR.PREFERENCES_PLEASE_RESTART_ANKI_TO_COMPLETE_LANGUAGE), parent=self
)
# Collection options
# Global: video driver
######################################################################
def setupCollection(self) -> None:
import anki.consts as c
f = self.form
qc = self.mw.col.conf
self.setup_video_driver()
f.newSpread.addItems(list(c.newCardSchedulingLabels(self.mw.col).values()))
f.useCurrent.setCurrentIndex(int(not qc.get("addToCur", True)))
s = self.prefs.scheduling
f.lrnCutoff.setValue(int(s.learn_ahead_secs / 60.0))
f.timeLimit.setValue(int(s.time_limit_secs / 60.0))
f.showEstimates.setChecked(s.show_intervals_on_buttons)
f.showProgress.setChecked(s.show_remaining_due_counts)
f.newSpread.setCurrentIndex(s.new_review_mix)
f.dayLearnFirst.setChecked(s.day_learn_first)
f.dayOffset.setValue(s.rollover)
if s.scheduler_version < 2:
f.dayLearnFirst.setVisible(False)
f.legacy_timezone.setVisible(False)
else:
f.legacy_timezone.setChecked(not s.new_timezone)
def setup_video_driver(self) -> None:
self.video_drivers = VideoDriver.all_for_platform()
names = [
@ -144,133 +298,17 @@ class Preferences(QDialog):
self.mw.pm.set_video_driver(new_driver)
showInfo(tr(TR.PREFERENCES_CHANGES_WILL_TAKE_EFFECT_WHEN_YOU))
def updateCollection(self) -> None:
f = self.form
d = self.mw.col
self.update_video_driver()
qc = d.conf
qc["addToCur"] = not f.useCurrent.currentIndex()
s = self.prefs.scheduling
s.show_remaining_due_counts = f.showProgress.isChecked()
s.show_intervals_on_buttons = f.showEstimates.isChecked()
s.new_review_mix = f.newSpread.currentIndex()
s.time_limit_secs = f.timeLimit.value() * 60
s.learn_ahead_secs = f.lrnCutoff.value() * 60
s.day_learn_first = f.dayLearnFirst.isChecked()
s.rollover = f.dayOffset.value()
s.new_timezone = not f.legacy_timezone.isChecked()
self.mw.col.set_preferences(self.prefs)
# Network
######################################################################
def setupNetwork(self) -> None:
self.form.media_log.setText(tr(TR.SYNC_MEDIA_LOG_BUTTON))
qconnect(self.form.media_log.clicked, self.on_media_log)
self.form.syncOnProgramOpen.setChecked(self.prof["autoSync"])
self.form.syncMedia.setChecked(self.prof["syncMedia"])
self.form.autoSyncMedia.setChecked(self.mw.pm.auto_sync_media_minutes() != 0)
if not self.prof["syncKey"]:
self._hideAuth()
def video_driver_name_for_platform(driver: VideoDriver) -> str:
if driver == VideoDriver.ANGLE:
return tr(TR.PREFERENCES_VIDEO_DRIVER_ANGLE)
elif driver == VideoDriver.Software:
if isMac:
return tr(TR.PREFERENCES_VIDEO_DRIVER_SOFTWARE_MAC)
else:
self.form.syncUser.setText(self.prof.get("syncUser", ""))
qconnect(self.form.syncDeauth.clicked, self.onSyncDeauth)
self.form.syncDeauth.setText(tr(TR.SYNC_LOG_OUT_BUTTON))
def on_media_log(self) -> None:
self.mw.media_syncer.show_sync_log()
def _hideAuth(self) -> None:
self.form.syncDeauth.setVisible(False)
self.form.syncUser.setText("")
self.form.syncLabel.setText(
tr(TR.PREFERENCES_SYNCHRONIZATIONNOT_CURRENTLY_ENABLED_CLICK_THE_SYNC)
)
def onSyncDeauth(self) -> None:
if self.mw.media_syncer.is_syncing():
showWarning("Can't log out while sync in progress.")
return
self.prof["syncKey"] = None
self.mw.col.media.force_resync()
self._hideAuth()
def updateNetwork(self) -> None:
self.prof["autoSync"] = self.form.syncOnProgramOpen.isChecked()
self.prof["syncMedia"] = self.form.syncMedia.isChecked()
self.mw.pm.set_auto_sync_media_minutes(
self.form.autoSyncMedia.isChecked() and 15 or 0
)
if self.form.fullSync.isChecked():
self.mw.col.modSchema(check=False)
# Backup
######################################################################
def setupBackup(self) -> None:
self.form.numBackups.setValue(self.prof["numBackups"])
def updateBackup(self) -> None:
self.prof["numBackups"] = self.form.numBackups.value()
# Basic & Advanced Options
######################################################################
def setupOptions(self) -> None:
self.form.pastePNG.setChecked(self.prof.get("pastePNG", False))
self.form.uiScale.setValue(int(self.mw.pm.uiScale() * 100))
self.form.pasteInvert.setChecked(self.prof.get("pasteInvert", False))
self.form.showPlayButtons.setChecked(self.prof.get("showPlayButtons", True))
self.form.nightMode.setChecked(self.mw.pm.night_mode())
self.form.interrupt_audio.setChecked(self.mw.pm.interrupt_audio())
self._recording_drivers = [
RecordingDriver.QtAudioInput,
RecordingDriver.PyAudio,
]
# The plan is to phase out PyAudio soon, so will hold off on
# making this string translatable for now.
self.form.recording_driver.addItems(
[
f"Voice recording driver: {driver.value}"
for driver in self._recording_drivers
]
)
self.form.recording_driver.setCurrentIndex(
self._recording_drivers.index(self.mw.pm.recording_driver())
)
def updateOptions(self) -> None:
restart_required = False
self.prof["pastePNG"] = self.form.pastePNG.isChecked()
self.prof["pasteInvert"] = self.form.pasteInvert.isChecked()
newScale = self.form.uiScale.value() / 100
if newScale != self.mw.pm.uiScale():
self.mw.pm.setUiScale(newScale)
restart_required = True
self.prof["showPlayButtons"] = self.form.showPlayButtons.isChecked()
if self.mw.pm.night_mode() != self.form.nightMode.isChecked():
self.mw.pm.set_night_mode(not self.mw.pm.night_mode())
restart_required = True
self.mw.pm.set_interrupt_audio(self.form.interrupt_audio.isChecked())
new_audio_driver = self._recording_drivers[
self.form.recording_driver.currentIndex()
]
if self.mw.pm.recording_driver() != new_audio_driver:
self.mw.pm.set_recording_driver(new_audio_driver)
if new_audio_driver == RecordingDriver.PyAudio:
showInfo(
"""\
The PyAudio driver will likely be removed in a future update. If you find it works better \
for you than the default driver, please let us know on the Anki forums."""
)
if restart_required:
showInfo(tr(TR.PREFERENCES_CHANGES_WILL_TAKE_EFFECT_WHEN_YOU))
return tr(TR.PREFERENCES_VIDEO_DRIVER_SOFTWARE_OTHER)
else:
if isMac:
return tr(TR.PREFERENCES_VIDEO_DRIVER_OPENGL_MAC)
else:
return tr(TR.PREFERENCES_VIDEO_DRIVER_OPENGL_OTHER)

View File

@ -89,14 +89,8 @@ profileConf: Dict[str, Any] = dict(
numBackups=50,
lastOptimize=intTime(),
# editing
fullSearch=False,
searchHistory=[],
lastColour="#00f",
stripHTML=True,
pastePNG=False,
# not exposed in gui
deleteMedia=False,
preserveKeyboard=True,
# syncing
syncKey=None,
syncMedia=True,
@ -104,6 +98,10 @@ profileConf: Dict[str, Any] = dict(
# importing
allowHTML=False,
importMode=1,
# these are not used, but Anki 2.1.42 and below
# expect these keys to exist
stripHTML=True,
deleteMedia=False,
)
@ -617,13 +615,6 @@ create table if not exists profiles
# Profile-specific
######################################################################
def interrupt_audio(self) -> bool:
return self.profile.get("interrupt_audio", True)
def set_interrupt_audio(self, val: bool) -> None:
self.profile["interrupt_audio"] = val
aqt.sound.av_player.interrupt_current_audio = val
def set_sync_key(self, val: Optional[str]) -> None:
self.profile["syncKey"] = val
@ -667,8 +658,3 @@ create table if not exists profiles
def set_recording_driver(self, driver: RecordingDriver) -> None:
self.profile["recordingDriver"] = driver.value
######################################################################
def apply_profile_options(self) -> None:
aqt.sound.av_player.interrupt_current_audio = self.interrupt_audio()

View File

@ -1036,16 +1036,27 @@ message Preferences {
uint32 rollover = 2;
uint32 learn_ahead_secs = 3;
NewReviewMix new_review_mix = 4;
bool show_remaining_due_counts = 5;
bool show_intervals_on_buttons = 6;
uint32 time_limit_secs = 7;
// v2 only
bool new_timezone = 8;
bool day_learn_first = 9;
bool new_timezone = 5;
bool day_learn_first = 6;
}
message Reviewing {
bool hide_audio_play_buttons = 1;
bool interrupt_audio_when_answering = 2;
bool show_remaining_due_counts = 3;
bool show_intervals_on_buttons = 4;
uint32 time_limit_secs = 5;
}
message Editing {
bool adding_defaults_to_current_deck = 1;
bool paste_images_as_png = 2;
bool paste_strips_formatting = 3;
}
Scheduling scheduling = 1;
Reviewing reviewing = 2;
Editing editing = 3;
}
message ClozeNumbersInNoteOut {
@ -1275,6 +1286,10 @@ message Config {
COLLAPSE_FLAGS = 8;
SCHED_2021 = 9;
ADDING_DEFAULTS_TO_CURRENT_DECK = 10;
HIDE_AUDIO_PLAY_BUTTONS = 11;
INTERRUPT_AUDIO_WHEN_ANSWERING = 12;
PASTE_IMAGES_AS_PNG = 13;
PASTE_STRIPS_FORMATTING = 14;
}
Key key = 1;
}

View File

@ -22,6 +22,10 @@ impl From<BoolKeyProto> for BoolKey {
BoolKeyProto::CollapseFlags => BoolKey::CollapseFlags,
BoolKeyProto::Sched2021 => BoolKey::Sched2021,
BoolKeyProto::AddingDefaultsToCurrentDeck => BoolKey::AddingDefaultsToCurrentDeck,
BoolKeyProto::HideAudioPlayButtons => BoolKey::HideAudioPlayButtons,
BoolKeyProto::InterruptAudioWhenAnswering => BoolKey::InterruptAudioWhenAnswering,
BoolKeyProto::PasteImagesAsPng => BoolKey::PasteImagesAsPng,
BoolKeyProto::PasteStripsFormatting => BoolKey::PasteStripsFormatting,
}
}
}

View File

@ -1319,7 +1319,7 @@ impl BackendService for Backend {
}
fn set_preferences(&self, input: pb::Preferences) -> BackendResult<Empty> {
self.with_col(|col| col.transact(None, |col| col.set_preferences(input)))
self.with_col(|col| col.set_preferences(input))
.map(Into::into)
}
}

View File

@ -18,6 +18,10 @@ pub enum BoolKey {
CollapseTags,
CollapseToday,
FutureDueShowBacklog,
HideAudioPlayButtons,
InterruptAudioWhenAnswering,
PasteImagesAsPng,
PasteStripsFormatting,
PreviewBothSides,
Sched2021,
@ -50,7 +54,9 @@ impl Collection {
}
// some keys default to true
BoolKey::AddingDefaultsToCurrentDeck
BoolKey::InterruptAudioWhenAnswering
| BoolKey::ShowIntervalsAboveAnswerButtons
| BoolKey::AddingDefaultsToCurrentDeck
| BoolKey::FutureDueShowBacklog
| BoolKey::ShowRemainingDueCountsInStudy
| BoolKey::CardCountsSeparateInactive

View File

@ -3,7 +3,9 @@
use crate::{
backend_proto::{
preferences::scheduling::NewReviewMix as NewRevMixPB, preferences::Scheduling, Preferences,
preferences::scheduling::NewReviewMix as NewRevMixPB,
preferences::{Editing, Reviewing, Scheduling},
Preferences,
},
collection::Collection,
config::BoolKey,
@ -14,19 +16,36 @@ use crate::{
impl Collection {
pub fn get_preferences(&self) -> Result<Preferences> {
Ok(Preferences {
scheduling: Some(self.get_collection_scheduling_settings()?),
scheduling: Some(self.get_scheduling_preferences()?),
reviewing: Some(self.get_reviewing_preferences()?),
editing: Some(self.get_editing_preferences()?),
})
}
pub fn set_preferences(&mut self, prefs: Preferences) -> Result<()> {
if let Some(sched) = prefs.scheduling {
self.set_collection_scheduling_settings(sched)?;
self.transact(
Some(crate::undo::UndoableOpKind::UpdatePreferences),
|col| col.set_preferences_inner(prefs),
)
}
fn set_preferences_inner(
&mut self,
prefs: Preferences,
) -> Result<(), crate::prelude::AnkiError> {
if let Some(sched) = prefs.scheduling {
self.set_scheduling_preferences(sched)?;
}
if let Some(reviewing) = prefs.reviewing {
self.set_reviewing_preferences(reviewing)?;
}
if let Some(editing) = prefs.editing {
self.set_editing_preferences(editing)?;
}
Ok(())
}
pub fn get_collection_scheduling_settings(&self) -> Result<Scheduling> {
pub fn get_scheduling_preferences(&self) -> Result<Scheduling> {
Ok(Scheduling {
scheduler_version: match self.scheduler_version() {
crate::config::SchedulerVersion::V1 => 1,
@ -39,30 +58,15 @@ impl Collection {
crate::config::NewReviewMix::ReviewsFirst => NewRevMixPB::ReviewsFirst,
crate::config::NewReviewMix::NewFirst => NewRevMixPB::NewFirst,
} as i32,
show_remaining_due_counts: self.get_bool(BoolKey::ShowRemainingDueCountsInStudy),
show_intervals_on_buttons: self.get_bool(BoolKey::ShowIntervalsAboveAnswerButtons),
time_limit_secs: self.get_answer_time_limit_secs(),
new_timezone: self.get_creation_utc_offset().is_some(),
day_learn_first: self.get_bool(BoolKey::ShowDayLearningCardsFirst),
})
}
pub(crate) fn set_collection_scheduling_settings(
&mut self,
settings: Scheduling,
) -> Result<()> {
pub(crate) fn set_scheduling_preferences(&mut self, settings: Scheduling) -> Result<()> {
let s = settings;
self.set_bool(BoolKey::ShowDayLearningCardsFirst, s.day_learn_first)?;
self.set_bool(
BoolKey::ShowRemainingDueCountsInStudy,
s.show_remaining_due_counts,
)?;
self.set_bool(
BoolKey::ShowIntervalsAboveAnswerButtons,
s.show_intervals_on_buttons,
)?;
self.set_answer_time_limit_secs(s.time_limit_secs)?;
self.set_learn_ahead_secs(s.learn_ahead_secs)?;
self.set_new_review_mix(match s.new_review_mix() {
@ -87,4 +91,52 @@ impl Collection {
Ok(())
}
pub fn get_reviewing_preferences(&self) -> Result<Reviewing> {
Ok(Reviewing {
hide_audio_play_buttons: self.get_bool(BoolKey::HideAudioPlayButtons),
interrupt_audio_when_answering: self.get_bool(BoolKey::InterruptAudioWhenAnswering),
show_remaining_due_counts: self.get_bool(BoolKey::ShowRemainingDueCountsInStudy),
show_intervals_on_buttons: self.get_bool(BoolKey::ShowIntervalsAboveAnswerButtons),
time_limit_secs: self.get_answer_time_limit_secs(),
})
}
pub(crate) fn set_reviewing_preferences(&mut self, settings: Reviewing) -> Result<()> {
let s = settings;
self.set_bool(BoolKey::HideAudioPlayButtons, s.hide_audio_play_buttons)?;
self.set_bool(
BoolKey::InterruptAudioWhenAnswering,
s.interrupt_audio_when_answering,
)?;
self.set_bool(
BoolKey::ShowRemainingDueCountsInStudy,
s.show_remaining_due_counts,
)?;
self.set_bool(
BoolKey::ShowIntervalsAboveAnswerButtons,
s.show_intervals_on_buttons,
)?;
self.set_answer_time_limit_secs(s.time_limit_secs)?;
Ok(())
}
pub fn get_editing_preferences(&self) -> Result<Editing> {
Ok(Editing {
adding_defaults_to_current_deck: self.get_bool(BoolKey::AddingDefaultsToCurrentDeck),
paste_images_as_png: self.get_bool(BoolKey::PasteImagesAsPng),
paste_strips_formatting: self.get_bool(BoolKey::PasteStripsFormatting),
})
}
pub(crate) fn set_editing_preferences(&mut self, settings: Editing) -> Result<()> {
let s = settings;
self.set_bool(
BoolKey::AddingDefaultsToCurrentDeck,
s.adding_defaults_to_current_deck,
)?;
self.set_bool(BoolKey::PasteImagesAsPng, s.paste_images_as_png)?;
self.set_bool(BoolKey::PasteStripsFormatting, s.paste_strips_formatting)?;
Ok(())
}
}

View File

@ -14,6 +14,7 @@ pub enum UndoableOpKind {
RemoveNote,
UpdateTag,
UpdateNote,
UpdatePreferences,
}
impl UndoableOpKind {
@ -34,6 +35,7 @@ impl Collection {
UndoableOpKind::RemoveNote => TR::StudyingDeleteNote,
UndoableOpKind::UpdateTag => TR::UndoUpdateTag,
UndoableOpKind::UpdateNote => TR::UndoUpdateNote,
UndoableOpKind::UpdatePreferences => TR::PreferencesPreferences,
};
self.i18n.tr(key).to_string()