From 24ad7c1f3535ba7ef9c05840646b1ee4f0039959 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Wed, 10 Mar 2021 14:11:59 +1000 Subject: [PATCH 01/13] inline scheduling settings into preferences --- qt/aqt/deckbrowser.py | 2 +- qt/aqt/preferences.py | 4 ++-- rslib/backend.proto | 44 ++++++++++++++++++++-------------------- rslib/src/preferences.rs | 13 ++++++------ 4 files changed, 31 insertions(+), 32 deletions(-) diff --git a/qt/aqt/deckbrowser.py b/qt/aqt/deckbrowser.py index 3977edf7b..565079f06 100644 --- a/qt/aqt/deckbrowser.py +++ b/qt/aqt/deckbrowser.py @@ -402,7 +402,7 @@ class DeckBrowser: defaultno=True, ): prefs = self.mw.col.get_preferences() - prefs.sched.new_timezone = False + prefs.scheduling.new_timezone = False self.mw.col.set_preferences(prefs) showInfo(tr(TR.SCHEDULING_UPDATE_DONE)) diff --git a/qt/aqt/preferences.py b/qt/aqt/preferences.py index aacd48a1d..ea9726c77 100644 --- a/qt/aqt/preferences.py +++ b/qt/aqt/preferences.py @@ -112,7 +112,7 @@ class Preferences(QDialog): f.useCurrent.setCurrentIndex(int(not qc.get("addToCur", True))) - s = self.prefs.sched + 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) @@ -153,7 +153,7 @@ class Preferences(QDialog): qc = d.conf qc["addToCur"] = not f.useCurrent.currentIndex() - s = self.prefs.sched + 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() diff --git a/rslib/backend.proto b/rslib/backend.proto index 608a52e6e..cbbdf3de1 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -1022,30 +1022,30 @@ message CheckDatabaseOut { repeated string problems = 1; } -message CollectionSchedulingSettings { - enum NewReviewMix { - DISTRIBUTE = 0; - REVIEWS_FIRST = 1; - NEW_FIRST = 2; +message Preferences { + message Scheduling { + enum NewReviewMix { + DISTRIBUTE = 0; + REVIEWS_FIRST = 1; + NEW_FIRST = 2; + } + + // read only + uint32 scheduler_version = 1; + + 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; } - // read only - uint32 scheduler_version = 1; - - 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; -} - -message Preferences { - CollectionSchedulingSettings sched = 1; + Scheduling scheduling = 1; } message ClozeNumbersInNoteOut { diff --git a/rslib/src/preferences.rs b/rslib/src/preferences.rs index ae45fb9b9..b2a59addd 100644 --- a/rslib/src/preferences.rs +++ b/rslib/src/preferences.rs @@ -3,8 +3,7 @@ use crate::{ backend_proto::{ - collection_scheduling_settings::NewReviewMix as NewRevMixPB, CollectionSchedulingSettings, - Preferences, + preferences::scheduling::NewReviewMix as NewRevMixPB, preferences::Scheduling, Preferences, }, collection::Collection, config::BoolKey, @@ -15,20 +14,20 @@ use crate::{ impl Collection { pub fn get_preferences(&self) -> Result { Ok(Preferences { - sched: Some(self.get_collection_scheduling_settings()?), + scheduling: Some(self.get_collection_scheduling_settings()?), }) } pub fn set_preferences(&mut self, prefs: Preferences) -> Result<()> { - if let Some(sched) = prefs.sched { + if let Some(sched) = prefs.scheduling { self.set_collection_scheduling_settings(sched)?; } Ok(()) } - pub fn get_collection_scheduling_settings(&self) -> Result { - Ok(CollectionSchedulingSettings { + pub fn get_collection_scheduling_settings(&self) -> Result { + Ok(Scheduling { scheduler_version: match self.scheduler_version() { crate::config::SchedulerVersion::V1 => 1, crate::config::SchedulerVersion::V2 => 2, @@ -50,7 +49,7 @@ impl Collection { pub(crate) fn set_collection_scheduling_settings( &mut self, - settings: CollectionSchedulingSettings, + settings: Scheduling, ) -> Result<()> { let s = settings; From 6b1dd9ee1968c301cc7d46f24b176712766c3c1d Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Wed, 10 Mar 2021 18:20:37 +1000 Subject: [PATCH 02/13] 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 --- qt/aqt/editor.py | 29 ++- qt/aqt/forms/preferences.ui | 4 +- qt/aqt/main.py | 17 +- qt/aqt/preferences.py | 412 ++++++++++++++++++++---------------- qt/aqt/profiles.py | 22 +- rslib/backend.proto | 25 ++- rslib/src/backend/config.rs | 4 + rslib/src/backend/mod.rs | 2 +- rslib/src/config/bool.rs | 8 +- rslib/src/preferences.rs | 96 +++++++-- rslib/src/undo/ops.rs | 2 + 11 files changed, 362 insertions(+), 259 deletions(-) diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index 3346dfbc6..ec2d69dd8 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -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: diff --git a/qt/aqt/forms/preferences.ui b/qt/aqt/forms/preferences.ui index 3e5293461..3e950546f 100644 --- a/qt/aqt/forms/preferences.ui +++ b/qt/aqt/forms/preferences.ui @@ -80,7 +80,7 @@ - + PREFERENCES_PASTE_WITHOUT_SHIFT_KEY_STRIPS_FORMATTING @@ -589,7 +589,7 @@ showPlayButtons interrupt_audio pastePNG - pasteInvert + paste_strips_formatting nightMode useCurrent recording_driver diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 6a99be3cc..6907c716c 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -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 ########################################################################## diff --git a/qt/aqt/preferences.py b/qt/aqt/preferences.py index ea9726c77..64fe935e9 100644 --- a/qt/aqt/preferences.py +++ b/qt/aqt/preferences.py @@ -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) diff --git a/qt/aqt/profiles.py b/qt/aqt/profiles.py index 50bc6a3a6..200e11f80 100644 --- a/qt/aqt/profiles.py +++ b/qt/aqt/profiles.py @@ -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() diff --git a/rslib/backend.proto b/rslib/backend.proto index cbbdf3de1..78e07aeae 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -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; } diff --git a/rslib/src/backend/config.rs b/rslib/src/backend/config.rs index fcac7d7cc..0deaa12e7 100644 --- a/rslib/src/backend/config.rs +++ b/rslib/src/backend/config.rs @@ -22,6 +22,10 @@ impl From 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, } } } diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index c8ee24a57..6c8d6c39b 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -1319,7 +1319,7 @@ impl BackendService for Backend { } fn set_preferences(&self, input: pb::Preferences) -> BackendResult { - self.with_col(|col| col.transact(None, |col| col.set_preferences(input))) + self.with_col(|col| col.set_preferences(input)) .map(Into::into) } } diff --git a/rslib/src/config/bool.rs b/rslib/src/config/bool.rs index 06f13e31e..ebb247d8b 100644 --- a/rslib/src/config/bool.rs +++ b/rslib/src/config/bool.rs @@ -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 diff --git a/rslib/src/preferences.rs b/rslib/src/preferences.rs index b2a59addd..e1b4eb80f 100644 --- a/rslib/src/preferences.rs +++ b/rslib/src/preferences.rs @@ -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 { 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 { + pub fn get_scheduling_preferences(&self) -> Result { 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 { + 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 { + 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(()) + } } diff --git a/rslib/src/undo/ops.rs b/rslib/src/undo/ops.rs index dea29e517..028549224 100644 --- a/rslib/src/undo/ops.rs +++ b/rslib/src/undo/ops.rs @@ -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() From e122f8ae0d622b8756dafca417880fe6b8c023a2 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Wed, 10 Mar 2021 23:50:11 +1000 Subject: [PATCH 03/13] undo support for deck adding/removing Work in progress - still to do: - renames appear as 'Update Deck' - easiest way to solve it would be to have a separate backend method for renames - drag&drop of decks not yet undoable - since the undo status is updated after the backend method ends, the older checkpoint() calls need to be replaced with an update_undo_status() at the end of the call - if we just remove the checkpoint, then the menu doesn't get updated --- ftl/core/undo.ftl | 2 + qt/aqt/deckbrowser.py | 4 +- qt/aqt/sidebar.py | 3 +- rslib/src/decks/mod.rs | 66 +++++++++++++++++++-------------- rslib/src/decks/undo.rs | 58 +++++++++++++++++++++++++++++ rslib/src/storage/deck/mod.rs | 4 +- rslib/src/storage/graves/mod.rs | 4 ++ rslib/src/undo/ops.rs | 22 +++++++---- 8 files changed, 121 insertions(+), 42 deletions(-) diff --git a/ftl/core/undo.ftl b/ftl/core/undo.ftl index e60c9a2a4..e3ae07497 100644 --- a/ftl/core/undo.ftl +++ b/ftl/core/undo.ftl @@ -11,7 +11,9 @@ undo-action-redone = { $action } redone undo-answer-card = Answer Card undo-unbury-unsuspend = Unbury/Unsuspend +undo-add-deck = Add Deck undo-add-note = Add Note undo-update-tag = Update Tag undo-update-note = Update Note undo-update-card = Update Card +undo-update-deck = Update Deck diff --git a/qt/aqt/deckbrowser.py b/qt/aqt/deckbrowser.py index 565079f06..96d839fe3 100644 --- a/qt/aqt/deckbrowser.py +++ b/qt/aqt/deckbrowser.py @@ -258,7 +258,6 @@ class DeckBrowser: self.mw.onExport(did=did) def _rename(self, did: int) -> None: - self.mw.checkpoint(tr(TR.ACTIONS_RENAME_DECK)) deck = self.mw.col.decks.get(did) oldName = deck["name"] newName = getOnlyText(tr(TR.DECKS_NEW_DECK_NAME), default=oldName) @@ -271,6 +270,7 @@ class DeckBrowser: except DeckIsFilteredError as err: showWarning(str(err)) return + self.mw.update_undo_actions() self.show() def _options(self, did: str) -> None: @@ -318,10 +318,10 @@ class DeckBrowser: return self.mw.col.decks.rem(did, True) def on_done(fut: Future) -> None: + self.mw.update_undo_actions() self.show() res = fut.result() # Required to check for errors - self.mw.checkpoint(tr(TR.DECKS_DELETE_DECK)) self.mw.taskman.with_progress(do_delete, on_done) # Top buttons diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 513144057..bb5e242b3 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -989,7 +989,6 @@ class SidebarTreeView(QTreeView): new_name = new_name.replace('"', "") if not new_name or new_name == old_name: return - self.mw.checkpoint(tr(TR.ACTIONS_RENAME_DECK)) try: self.mw.col.decks.rename(deck, new_name) except DeckIsFilteredError as err: @@ -997,6 +996,7 @@ class SidebarTreeView(QTreeView): return self.refresh() self.mw.deckBrowser.refresh() + self.mw.update_undo_actions() def remove_tag(self, item: SidebarItem) -> None: self.browser.editor.saveNow(lambda: self._remove_tag(item)) @@ -1063,7 +1063,6 @@ class SidebarTreeView(QTreeView): self.refresh() res = fut.result() # Required to check for errors - self.mw.checkpoint(tr(TR.DECKS_DELETE_DECK)) self.browser.model.beginReset() self.mw.taskman.run_in_background(do_delete, on_done) diff --git a/rslib/src/decks/mod.rs b/rslib/src/decks/mod.rs index 8a70f5470..16cd714b5 100644 --- a/rslib/src/decks/mod.rs +++ b/rslib/src/decks/mod.rs @@ -10,7 +10,10 @@ pub use crate::backend_proto::{ deck_kind::Kind as DeckKind, filtered_search_term::FilteredSearchOrder, Deck as DeckProto, DeckCommon, DeckKind as DeckKindProto, FilteredDeck, FilteredSearchTerm, NormalDeck, }; -use crate::{backend_proto as pb, markdown::render_markdown, text::sanitize_html_no_images}; +use crate::{ + backend_proto as pb, markdown::render_markdown, text::sanitize_html_no_images, + undo::UndoableOpKind, +}; use crate::{ collection::Collection, deckconf::DeckConfID, @@ -263,21 +266,38 @@ impl Collection { } /// Add or update an existing deck modified by the user. May add parents, - /// or rename children as required. + /// or rename children as required. Prefer add_deck() or update_deck() to + /// be explicit about your intentions; this function mainly exists so we + /// can integrate with older Python code that behaved this way. pub(crate) fn add_or_update_deck(&mut self, deck: &mut Deck) -> Result<()> { - self.state.deck_cache.clear(); + if deck.id.0 == 0 { + self.add_deck(deck) + } else { + self.update_deck(deck) + } + } - self.transact(None, |col| { + /// Add a new deck. The id must be 0, as it will be automatically assigned. + pub fn add_deck(&mut self, deck: &mut Deck) -> Result<()> { + if deck.id.0 != 0 { + return Err(AnkiError::invalid_input("deck to add must have id 0")); + } + + self.transact(Some(UndoableOpKind::AddDeck), |col| { let usn = col.usn()?; - col.prepare_deck_for_update(deck, usn)?; deck.set_modified(usn); + col.match_or_create_parents(deck, usn)?; + col.add_deck_undoable(deck) + }) + } - if deck.id.0 == 0 { - // TODO: undo support - col.match_or_create_parents(deck, usn)?; - col.storage.add_deck(deck) - } else if let Some(existing_deck) = col.storage.get_deck(deck.id)? { + pub(crate) fn update_deck(&mut self, deck: &mut Deck) -> Result<()> { + self.transact(Some(UndoableOpKind::UpdateDeck), |col| { + let usn = col.usn()?; + col.prepare_deck_for_update(deck, usn)?; + deck.set_modified(usn); + if let Some(existing_deck) = col.storage.get_deck(deck.id)? { let name_changed = existing_deck.name != deck.name; if name_changed { // match closest parent name @@ -301,15 +321,13 @@ impl Collection { /// Add/update a single deck when syncing/importing. Ensures name is unique /// & normalized, but does not check parents/children or update mtime /// (unless the name was changed). Caller must set up transaction. - /// TODO: undo support pub(crate) fn add_or_update_single_deck_with_existing_id( &mut self, deck: &mut Deck, usn: Usn, ) -> Result<()> { - self.state.deck_cache.clear(); self.prepare_deck_for_update(deck, usn)?; - self.storage.add_or_update_deck_with_existing_id(deck) + self.add_or_update_deck_with_existing_id_undoable(deck) } pub(crate) fn ensure_deck_name_unique(&self, deck: &mut Deck, usn: Usn) -> Result<()> { @@ -367,12 +385,11 @@ impl Collection { /// Add a single, normal deck with the provided name for a child deck. /// Caller must have done necessarily validation on name. - fn add_parent_deck(&self, machine_name: &str, usn: Usn) -> Result<()> { + fn add_parent_deck(&mut self, machine_name: &str, usn: Usn) -> Result<()> { let mut deck = Deck::new_normal(); deck.name = machine_name.into(); deck.set_modified(usn); - // fixme: undo - self.storage.add_deck(&mut deck) + self.add_deck_undoable(&mut deck) } /// If parent deck(s) exist, rewrite name to match their case. @@ -404,7 +421,7 @@ impl Collection { } } - fn create_missing_parents(&self, mut machine_name: &str, usn: Usn) -> Result<()> { + fn create_missing_parents(&mut self, mut machine_name: &str, usn: Usn) -> Result<()> { while let Some(parent_name) = immediate_parent_name(machine_name) { if self.storage.get_deck_id(parent_name)?.is_none() { self.add_parent_deck(parent_name, usn)?; @@ -441,12 +458,8 @@ impl Collection { } pub fn remove_deck_and_child_decks(&mut self, did: DeckID) -> Result<()> { - // fixme: vet cache clearing - self.state.deck_cache.clear(); - - self.transact(None, |col| { + self.transact(Some(UndoableOpKind::RemoveDeck), |col| { let usn = col.usn()?; - if let Some(deck) = col.storage.get_deck(did)? { let child_decks = col.storage.child_decks(&deck)?; @@ -463,23 +476,20 @@ impl Collection { } pub(crate) fn remove_single_deck(&mut self, deck: &Deck, usn: Usn) -> Result<()> { - // fixme: undo match deck.kind { DeckKind::Normal(_) => self.delete_all_cards_in_normal_deck(deck.id)?, DeckKind::Filtered(_) => self.return_all_cards_in_filtered_deck(deck.id)?, } self.clear_aux_config_for_deck(deck.id)?; if deck.id.0 == 1 { + // if deleting the default deck, ensure there's a new one, and avoid the grave let mut deck = deck.to_owned(); - // fixme: separate key deck.name = self.i18n.tr(TR::DeckConfigDefaultName).into(); deck.set_modified(usn); - self.add_or_update_single_deck_with_existing_id(&mut deck, usn)?; + self.add_or_update_single_deck_with_existing_id(&mut deck, usn) } else { - self.storage.remove_deck(deck.id)?; - self.storage.add_deck_grave(deck.id, usn)?; + self.remove_deck_and_add_grave_undoable(deck.clone(), usn) } - Ok(()) } fn delete_all_cards_in_normal_deck(&mut self, did: DeckID) -> Result<()> { diff --git a/rslib/src/decks/undo.rs b/rslib/src/decks/undo.rs index 3251a0a91..e4fe37749 100644 --- a/rslib/src/decks/undo.rs +++ b/rslib/src/decks/undo.rs @@ -6,12 +6,17 @@ use crate::prelude::*; #[derive(Debug)] pub(crate) enum UndoableDeckChange { + Added(Box), Updated(Box), + Removed(Box), + GraveAdded(Box<(DeckID, Usn)>), + GraveRemoved(Box<(DeckID, Usn)>), } impl Collection { pub(crate) fn undo_deck_change(&mut self, change: UndoableDeckChange) -> Result<()> { match change { + UndoableDeckChange::Added(deck) => self.remove_deck_undoable(*deck), UndoableDeckChange::Updated(mut deck) => { let current = self .storage @@ -19,9 +24,28 @@ impl Collection { .ok_or_else(|| AnkiError::invalid_input("deck disappeared"))?; self.update_single_deck_undoable(&mut *deck, ¤t) } + UndoableDeckChange::Removed(deck) => self.restore_deleted_deck(*deck), + UndoableDeckChange::GraveAdded(e) => self.remove_deck_grave(e.0, e.1), + UndoableDeckChange::GraveRemoved(e) => self.add_deck_grave_undoable(e.0, e.1), } } + pub(super) fn add_deck_undoable(&mut self, deck: &mut Deck) -> Result<(), AnkiError> { + self.storage.add_deck(deck)?; + self.save_undo(UndoableDeckChange::Added(Box::new(deck.clone()))); + Ok(()) + } + + pub(super) fn add_or_update_deck_with_existing_id_undoable( + &mut self, + deck: &mut Deck, + ) -> Result<(), AnkiError> { + self.state.deck_cache.clear(); + self.storage.add_or_update_deck_with_existing_id(deck)?; + self.save_undo(UndoableDeckChange::Added(Box::new(deck.clone()))); + Ok(()) + } + /// Update an individual, existing deck. Caller is responsible for ensuring deck /// is normalized, matches parents, is not a duplicate name, and bumping mtime. /// Clears deck cache. @@ -34,4 +58,38 @@ impl Collection { self.save_undo(UndoableDeckChange::Updated(Box::new(original.clone()))); self.storage.update_deck(deck) } + + pub(crate) fn remove_deck_and_add_grave_undoable( + &mut self, + deck: Deck, + usn: Usn, + ) -> Result<()> { + self.state.deck_cache.clear(); + self.add_deck_grave_undoable(deck.id, usn)?; + self.storage.remove_deck(deck.id)?; + self.save_undo(UndoableDeckChange::Removed(Box::new(deck))); + Ok(()) + } + + fn restore_deleted_deck(&mut self, deck: Deck) -> Result<()> { + self.storage.add_or_update_deck_with_existing_id(&deck)?; + self.save_undo(UndoableDeckChange::Added(Box::new(deck))); + Ok(()) + } + + fn remove_deck_undoable(&mut self, deck: Deck) -> Result<()> { + self.storage.remove_deck(deck.id)?; + self.save_undo(UndoableDeckChange::Removed(Box::new(deck))); + Ok(()) + } + + fn add_deck_grave_undoable(&mut self, did: DeckID, usn: Usn) -> Result<()> { + self.save_undo(UndoableDeckChange::GraveAdded(Box::new((did, usn)))); + self.storage.add_deck_grave(did, usn) + } + + fn remove_deck_grave(&mut self, did: DeckID, usn: Usn) -> Result<()> { + self.save_undo(UndoableDeckChange::GraveRemoved(Box::new((did, usn)))); + self.storage.remove_deck_grave(did) + } } diff --git a/rslib/src/storage/deck/mod.rs b/rslib/src/storage/deck/mod.rs index 14e50597c..e2f76e88a 100644 --- a/rslib/src/storage/deck/mod.rs +++ b/rslib/src/storage/deck/mod.rs @@ -138,8 +138,8 @@ impl SqliteStorage { } } - /// Used for syncing; will keep existing ID. Shouldn't be used to add new decks locally, - /// since it does not allocate an id. + /// Used for syncing&undo; will keep existing ID. Shouldn't be used to add + /// new decks locally, since it does not allocate an id. pub(crate) fn add_or_update_deck_with_existing_id(&self, deck: &Deck) -> Result<()> { if deck.id.0 == 0 { return Err(AnkiError::invalid_input("deck with id 0")); diff --git a/rslib/src/storage/graves/mod.rs b/rslib/src/storage/graves/mod.rs index e9884126f..b8d9c0021 100644 --- a/rslib/src/storage/graves/mod.rs +++ b/rslib/src/storage/graves/mod.rs @@ -48,6 +48,10 @@ impl SqliteStorage { self.remove_grave(nid.0, GraveKind::Note) } + pub(crate) fn remove_deck_grave(&self, did: DeckID) -> Result<()> { + self.remove_grave(did.0, GraveKind::Deck) + } + pub(crate) fn pending_graves(&self, pending_usn: Usn) -> Result { let mut stmt = self.db.prepare(&format!( "select oid, type from graves where {}", diff --git a/rslib/src/undo/ops.rs b/rslib/src/undo/ops.rs index 028549224..d708ef943 100644 --- a/rslib/src/undo/ops.rs +++ b/rslib/src/undo/ops.rs @@ -5,16 +5,19 @@ use crate::prelude::*; #[derive(Debug, Clone, Copy, PartialEq)] pub enum UndoableOpKind { - UpdateCard, + AddDeck, + AddNote, AnswerCard, Bury, + RemoveDeck, + RemoveNote, Suspend, UnburyUnsuspend, - AddNote, - RemoveNote, - UpdateTag, + UpdateCard, + UpdateDeck, UpdateNote, UpdatePreferences, + UpdateTag, } impl UndoableOpKind { @@ -26,16 +29,19 @@ impl UndoableOpKind { impl Collection { pub fn describe_op_kind(&self, op: UndoableOpKind) -> String { let key = match op { - UndoableOpKind::UpdateCard => TR::UndoUpdateCard, + UndoableOpKind::AddDeck => TR::UndoAddDeck, + UndoableOpKind::AddNote => TR::UndoAddNote, UndoableOpKind::AnswerCard => TR::UndoAnswerCard, UndoableOpKind::Bury => TR::StudyingBury, + UndoableOpKind::RemoveDeck => TR::DecksDeleteDeck, + UndoableOpKind::RemoveNote => TR::StudyingDeleteNote, UndoableOpKind::Suspend => TR::StudyingSuspend, UndoableOpKind::UnburyUnsuspend => TR::UndoUnburyUnsuspend, - UndoableOpKind::AddNote => TR::UndoAddNote, - UndoableOpKind::RemoveNote => TR::StudyingDeleteNote, - UndoableOpKind::UpdateTag => TR::UndoUpdateTag, + UndoableOpKind::UpdateCard => TR::UndoUpdateCard, + UndoableOpKind::UpdateDeck => TR::UndoUpdateDeck, UndoableOpKind::UpdateNote => TR::UndoUpdateNote, UndoableOpKind::UpdatePreferences => TR::PreferencesPreferences, + UndoableOpKind::UpdateTag => TR::UndoUpdateTag, }; self.i18n.tr(key).to_string() From c3b0589bb41bb8330a32603101d1c72773920922 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Wed, 10 Mar 2021 23:59:40 +1000 Subject: [PATCH 04/13] make sure we invalidate cache when undoing deck add --- rslib/src/decks/undo.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/rslib/src/decks/undo.rs b/rslib/src/decks/undo.rs index e4fe37749..bd826becf 100644 --- a/rslib/src/decks/undo.rs +++ b/rslib/src/decks/undo.rs @@ -78,6 +78,7 @@ impl Collection { } fn remove_deck_undoable(&mut self, deck: Deck) -> Result<()> { + self.state.deck_cache.clear(); self.storage.remove_deck(deck.id)?; self.save_undo(UndoableDeckChange::Removed(Box::new(deck))); Ok(()) From 76102db2a92b4b972aa48fcbb6b7f4253298c89c Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Wed, 10 Mar 2021 19:28:08 +0100 Subject: [PATCH 05/13] Fix bug with initially enabled toolbar in browser --- ts/editor/index.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ts/editor/index.ts b/ts/editor/index.ts index 3e02ffa43..0f0c87f2e 100644 --- a/ts/editor/index.ts +++ b/ts/editor/index.ts @@ -4,7 +4,7 @@ import { caretToEnd } from "./helpers"; import { saveField } from "./changeTimer"; import { filterHTML } from "./htmlFilter"; -import { updateButtonState } from "./toolbar"; +import { updateButtonState, disableButtons } from "./toolbar"; import { EditorField } from "./editorField"; import { LabelContainer } from "./labelContainer"; @@ -113,6 +113,11 @@ export function setFields(fields: [string, string][]): void { forEditorField(fields, (field, [name, fieldContent]) => field.initialize(name, color, fieldContent) ); + + if (!getCurrentField()) { + // when initial focus of the window is not on editor (e.g. browser) + disableButtons(); + } } export function setBackgrounds(cols: ("dupe" | "")[]): void { From 38ae2f2b06776f43b511dc5b469f346d26ad8bbe Mon Sep 17 00:00:00 2001 From: bluegreenmagick Date: Thu, 11 Mar 2021 09:04:24 +0900 Subject: [PATCH 06/13] Clicking the expander icon should not trigger click event Expander icon does not change currentIndex --- qt/aqt/sidebar.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index bb5e242b3..4db53abaa 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -443,7 +443,8 @@ class SidebarTreeView(QTreeView): super().mouseReleaseEvent(event) if event.button() == Qt.LeftButton: idx = self.indexAt(event.pos()) - self._on_click_index(idx) + if idx == self.currentIndex(): + self._on_click_index(idx) def keyPressEvent(self, event: QKeyEvent) -> None: if event.key() in (Qt.Key_Return, Qt.Key_Enter): From 5df684fa6b0fc3f2b58310749d838702f557b315 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 11 Mar 2021 14:33:57 +1000 Subject: [PATCH 07/13] rework backend codegen to support multiple services; split out sched Rust requires all methods of impl Trait to be in a single file, which means we had a giant backend/mod.rs covering all exposed methods. By using separate service definitions for the separate areas, and updating the code generation, we can split it into more manageable chunks - this commit starts with the scheduling code. In the long run, we'll probably want to split up the protobuf file into multiple files as well. Also dropped want_release_gil() from rsbridge, and the associated method enum. While it allows us to skip the thread save/restore and mutex unlock/ lock, it looks to only be buying about 2.5% extra performance in the best case (tested with timeit+format_timespan), and the majority of the backend methods deal with I/O, and thus were already releasing the GIL. --- pylib/anki/_backend/BUILD.bazel | 2 +- pylib/anki/_backend/__init__.py | 4 +- pylib/anki/_backend/genbackend.py | 31 ++- pylib/anki/_backend/rsbridge.pyi | 2 +- pylib/rsbridge/lib.rs | 60 ++--- rslib/backend.proto | 56 +++-- rslib/build/protobuf.rs | 36 ++- rslib/src/backend/mod.rs | 384 ++++++++--------------------- rslib/src/backend/scheduler/mod.rs | 172 +++++++++++++ 9 files changed, 366 insertions(+), 381 deletions(-) diff --git a/pylib/anki/_backend/BUILD.bazel b/pylib/anki/_backend/BUILD.bazel index c2667acf5..08bef7231 100644 --- a/pylib/anki/_backend/BUILD.bazel +++ b/pylib/anki/_backend/BUILD.bazel @@ -36,7 +36,7 @@ py_binary( genrule( name = "rsbackend_gen", outs = ["generated.py"], - cmd = "$(location genbackend) > $@", + cmd = "$(location genbackend) $@", tools = ["genbackend"], ) diff --git a/pylib/anki/_backend/__init__.py b/pylib/anki/_backend/__init__.py index d27f32c3a..92d07818a 100644 --- a/pylib/anki/_backend/__init__.py +++ b/pylib/anki/_backend/__init__.py @@ -95,10 +95,10 @@ class RustBackend(RustBackendGenerated): ) return self.format_timespan(seconds=seconds, context=context) - def _run_command(self, method: int, input: Any) -> bytes: + def _run_command(self, service: int, method: int, input: Any) -> bytes: input_bytes = input.SerializeToString() try: - return self._backend.command(method, input_bytes) + return self._backend.command(service, method, input_bytes) except Exception as e: err_bytes = bytes(e.args[0]) err = pb.BackendError() diff --git a/pylib/anki/_backend/genbackend.py b/pylib/anki/_backend/genbackend.py index 808c31689..31c44ee86 100755 --- a/pylib/anki/_backend/genbackend.py +++ b/pylib/anki/_backend/genbackend.py @@ -1,10 +1,13 @@ #!/usr/bin/env python3 # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + import os import re import sys +import google.protobuf.descriptor + import pylib.anki._backend.backend_pb2 as pb import stringcase @@ -97,7 +100,7 @@ def get_input_assign(msg): return ", ".join(f"{f.name}={f.name}" for f in fields) -def render_method(method, idx): +def render_method(service_idx, method_idx, method): input_name = method.input_type.name if ( (input_name.endswith("In") or len(method.input_type.fields) < 2) @@ -134,11 +137,11 @@ def render_method(method, idx): {input_assign_outer}""" if method.name in SKIP_DECODE: - buf += f"""return self._run_command({idx+1}, input) + buf += f"""return self._run_command({service_idx}, {method_idx+1}, input) """ else: buf += f"""output = pb.{method.output_type.name}() - output.ParseFromString(self._run_command({idx+1}, input)) + output.ParseFromString(self._run_command({service_idx}, {method_idx+1}, input)) return output{single_field} """ @@ -146,13 +149,27 @@ def render_method(method, idx): out = [] -for idx, method in enumerate(pb._BACKENDSERVICE.methods): - out.append(render_method(method, idx)) + + +def render_service( + service: google.protobuf.descriptor.ServiceDescriptor, service_index: int +) -> None: + for method_index, method in enumerate(service.methods): + out.append(render_method(service_index, method_index, method)) + + +for service in pb.ServiceIndex.DESCRIPTOR.values: + # SERVICE_INDEX_TEST -> _TESTSERVICE + service_var = service.name.replace("SERVICE_INDEX", "") + "SERVICE" + service_obj = getattr(pb, service_var) + service_index = service.number + render_service(service_obj, service_index) + out = "\n".join(out) -sys.stdout.buffer.write( +open(sys.argv[1], "wb").write( ( '''# Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html @@ -174,7 +191,7 @@ from typing import * import anki._backend.backend_pb2 as pb class RustBackendGenerated: - def _run_command(self, method: int, input: Any) -> bytes: + def _run_command(self, service: int, method: int, input: Any) -> bytes: raise Exception("not implemented") ''' diff --git a/pylib/anki/_backend/rsbridge.pyi b/pylib/anki/_backend/rsbridge.pyi index 126678bc9..9d0e23d6a 100644 --- a/pylib/anki/_backend/rsbridge.pyi +++ b/pylib/anki/_backend/rsbridge.pyi @@ -3,5 +3,5 @@ def open_backend(data: bytes) -> Backend: ... class Backend: @classmethod - def command(self, method: int, data: bytes) -> bytes: ... + def command(self, service: int, method: int, data: bytes) -> bytes: ... def db_command(self, data: bytes) -> bytes: ... diff --git a/pylib/rsbridge/lib.rs b/pylib/rsbridge/lib.rs index 6c780f751..b7ce73955 100644 --- a/pylib/rsbridge/lib.rs +++ b/pylib/rsbridge/lib.rs @@ -1,15 +1,11 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use anki::backend::{init_backend, Backend as RustBackend, BackendMethod}; +use anki::backend::{init_backend, Backend as RustBackend}; use pyo3::exceptions::PyException; use pyo3::prelude::*; use pyo3::types::PyBytes; use pyo3::{create_exception, wrap_pyfunction}; -use std::convert::TryFrom; - -// Regular backend -////////////////////////////////// #[pyclass(module = "rsbridge")] struct Backend { @@ -31,50 +27,22 @@ fn open_backend(init_msg: &PyBytes) -> PyResult { } } -fn want_release_gil(method: u32) -> bool { - if let Ok(method) = BackendMethod::try_from(method) { - !matches!( - method, - BackendMethod::ExtractAVTags - | BackendMethod::ExtractLatex - | BackendMethod::RenderExistingCard - | BackendMethod::RenderUncommittedCard - | BackendMethod::StripAVTags - | BackendMethod::SchedTimingToday - | BackendMethod::AddOrUpdateDeckLegacy - | BackendMethod::NewDeckLegacy - | BackendMethod::NewDeckConfigLegacy - | BackendMethod::GetStockNotetypeLegacy - | BackendMethod::StudiedToday - | BackendMethod::TranslateString - | BackendMethod::FormatTimespan - | BackendMethod::LatestProgress - | BackendMethod::SetWantsAbort - | BackendMethod::I18nResources - | BackendMethod::JoinSearchNodes - | BackendMethod::ReplaceSearchNode - | BackendMethod::BuildSearchString - | BackendMethod::StateIsLeech - ) - } else { - false - } -} - #[pymethods] impl Backend { - fn command(&self, py: Python, method: u32, input: &PyBytes) -> PyResult { + fn command( + &self, + py: Python, + service: u32, + method: u32, + input: &PyBytes, + ) -> PyResult { let in_bytes = input.as_bytes(); - if want_release_gil(method) { - py.allow_threads(|| self.backend.run_command_bytes(method, in_bytes)) - } else { - self.backend.run_command_bytes(method, in_bytes) - } - .map(|out_bytes| { - let out_obj = PyBytes::new(py, &out_bytes); - out_obj.into() - }) - .map_err(BackendError::new_err) + py.allow_threads(|| self.backend.run_method(service, method, in_bytes)) + .map(|out_bytes| { + let out_obj = PyBytes::new(py, &out_bytes); + out_obj.into() + }) + .map_err(BackendError::new_err) } /// This takes and returns JSON, due to Python's slow protobuf diff --git a/rslib/backend.proto b/rslib/backend.proto index 78e07aeae..11a390895 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -72,33 +72,18 @@ message DeckConfigID { int64 dcid = 1; } -// New style RPC definitions +// Backend methods /////////////////////////////////////////////////////////// -service BackendService { - rpc LatestProgress(Empty) returns (Progress); - rpc SetWantsAbort(Empty) returns (Empty); - - // card rendering - - rpc ExtractAVTags(ExtractAVTagsIn) returns (ExtractAVTagsOut); - rpc ExtractLatex(ExtractLatexIn) returns (ExtractLatexOut); - rpc GetEmptyCards(Empty) returns (EmptyCardsReport); - rpc RenderExistingCard(RenderExistingCardIn) returns (RenderCardOut); - rpc RenderUncommittedCard(RenderUncommittedCardIn) returns (RenderCardOut); - rpc StripAVTags(String) returns (String); - - // searching - - rpc BuildSearchString(SearchNode) returns (String); - rpc SearchCards(SearchCardsIn) returns (SearchCardsOut); - rpc SearchNotes(SearchNotesIn) returns (SearchNotesOut); - rpc JoinSearchNodes(JoinSearchNodesIn) returns (String); - rpc ReplaceSearchNode(ReplaceSearchNodeIn) returns (String); - rpc FindAndReplace(FindAndReplaceIn) returns (UInt32); - - // scheduling +/// while the protobuf descriptors expose the order services are defined in, +/// that information is not available in prost, so we define an enum to make +/// sure all clients agree on the same service indices +enum ServiceIndex { + SERVICE_INDEX_SCHEDULING = 0; + SERVICE_INDEX_BACKEND = 1; +} +service SchedulingService { rpc SchedTimingToday(Empty) returns (SchedTimingTodayOut); rpc StudiedToday(Empty) returns (String); rpc StudiedTodayMessage(StudiedTodayMessageIn) returns (String); @@ -121,6 +106,29 @@ service BackendService { rpc AnswerCard(AnswerCardIn) returns (Empty); rpc UpgradeScheduler(Empty) returns (Empty); rpc GetQueuedCards(GetQueuedCardsIn) returns (GetQueuedCardsOut); +} + +service BackendService { + rpc LatestProgress(Empty) returns (Progress); + rpc SetWantsAbort(Empty) returns (Empty); + + // card rendering + + rpc ExtractAVTags(ExtractAVTagsIn) returns (ExtractAVTagsOut); + rpc ExtractLatex(ExtractLatexIn) returns (ExtractLatexOut); + rpc GetEmptyCards(Empty) returns (EmptyCardsReport); + rpc RenderExistingCard(RenderExistingCardIn) returns (RenderCardOut); + rpc RenderUncommittedCard(RenderUncommittedCardIn) returns (RenderCardOut); + rpc StripAVTags(String) returns (String); + + // searching + + rpc BuildSearchString(SearchNode) returns (String); + rpc SearchCards(SearchCardsIn) returns (SearchCardsOut); + rpc SearchNotes(SearchNotesIn) returns (SearchNotesOut); + rpc JoinSearchNodes(JoinSearchNodesIn) returns (String); + rpc ReplaceSearchNode(ReplaceSearchNodeIn) returns (String); + rpc FindAndReplace(FindAndReplaceIn) returns (UInt32); // stats diff --git a/rslib/build/protobuf.rs b/rslib/build/protobuf.rs index 93143af4a..aad63055b 100644 --- a/rslib/build/protobuf.rs +++ b/rslib/build/protobuf.rs @@ -6,32 +6,14 @@ use std::{env, fmt::Write}; struct CustomGenerator {} -fn write_method_enum(buf: &mut String, service: &prost_build::Service) { - buf.push_str( - r#" -use num_enum::TryFromPrimitive; -#[derive(PartialEq,TryFromPrimitive)] -#[repr(u32)] -pub enum BackendMethod { -"#, - ); - for (idx, method) in service.methods.iter().enumerate() { - writeln!(buf, " {} = {},", method.proto_name, idx + 1).unwrap(); - } - buf.push_str("}\n\n"); -} - fn write_method_trait(buf: &mut String, service: &prost_build::Service) { buf.push_str( r#" -use prost::Message; -pub type BackendResult = std::result::Result; -pub trait BackendService { - fn run_command_bytes2_inner(&self, method: u32, input: &[u8]) -> std::result::Result, crate::err::AnkiError> { +pub trait Service { + fn run_method(&self, method: u32, input: &[u8]) -> Result> { match method { "#, ); - for (idx, method) in service.methods.iter().enumerate() { write!( buf, @@ -58,7 +40,7 @@ pub trait BackendService { buf, concat!( " fn {method_name}(&self, input: {input_type}) -> ", - "BackendResult<{output_type}>;\n" + "Result<{output_type}>;\n" ), method_name = method.name, input_type = method.input_type, @@ -71,8 +53,18 @@ pub trait BackendService { impl prost_build::ServiceGenerator for CustomGenerator { fn generate(&mut self, service: prost_build::Service, buf: &mut String) { - write_method_enum(buf, &service); + write!( + buf, + "pub mod {name} {{ + use super::*; + use prost::Message; + use crate::err::Result; + ", + name = service.name.replace("Service", "").to_ascii_lowercase() + ) + .unwrap(); write_method_trait(buf, &service); + buf.push('}'); } } diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 6c8d6c39b..da51c86a5 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -13,13 +13,13 @@ mod scheduler; mod search; mod sync; -pub use crate::backend_proto::BackendMethod; +use self::scheduler::SchedulingService; +use crate::backend_proto::backend::Service as BackendService; + use crate::{ backend::dbproxy::db_command_bytes, backend_proto as pb, - backend_proto::{ - AddOrUpdateDeckConfigLegacyIn, BackendResult, Empty, RenderedTemplateReplacement, - }, + backend_proto::{AddOrUpdateDeckConfigLegacyIn, Empty, RenderedTemplateReplacement}, card::{Card, CardID}, cloze::add_cloze_numbers_in_string, collection::{open_collection, Collection}, @@ -37,14 +37,8 @@ use crate::{ notetype::{ all_stock_notetypes, CardTemplateSchema11, NoteType, NoteTypeSchema11, RenderCardOutput, }, - scheduler::{ - new::NewCardSortOrder, - parse_due_date_str, - states::{CardState, NextCardStates}, - timespan::{answer_button_time, time_span}, - }, + scheduler::timespan::{answer_button_time, time_span}, search::{concatenate_searches, replace_search_node, write_nodes, Node}, - stats::studied_today, sync::{http::SyncRequest, LocalServer}, template::RenderedNode, text::{extract_av_tags, sanitize_html_no_images, strip_av_tags, AVTag}, @@ -55,7 +49,6 @@ use fluent::FluentValue; use futures::future::AbortHandle; use log::error; use once_cell::sync::OnceCell; -use pb::BackendService; use progress::{AbortHandleSlot, Progress}; use prost::Message; use serde_json::Value as JsonValue; @@ -107,19 +100,19 @@ pub fn init_backend(init_msg: &[u8]) -> std::result::Result { } impl BackendService for Backend { - fn latest_progress(&self, _input: Empty) -> BackendResult { + fn latest_progress(&self, _input: Empty) -> Result { let progress = self.progress_state.lock().unwrap().last_progress; Ok(progress_to_proto(progress, &self.i18n)) } - fn set_wants_abort(&self, _input: Empty) -> BackendResult { + fn set_wants_abort(&self, _input: Empty) -> Result { self.progress_state.lock().unwrap().want_abort = true; Ok(().into()) } // card rendering - fn extract_av_tags(&self, input: pb::ExtractAvTagsIn) -> BackendResult { + fn extract_av_tags(&self, input: pb::ExtractAvTagsIn) -> Result { let (text, tags) = extract_av_tags(&input.text, input.question_side); let pt_tags = tags .into_iter() @@ -151,7 +144,7 @@ impl BackendService for Backend { }) } - fn extract_latex(&self, input: pb::ExtractLatexIn) -> BackendResult { + fn extract_latex(&self, input: pb::ExtractLatexIn) -> Result { let func = if input.expand_clozes { extract_latex_expanding_clozes } else { @@ -193,10 +186,7 @@ impl BackendService for Backend { }) } - fn render_existing_card( - &self, - input: pb::RenderExistingCardIn, - ) -> BackendResult { + fn render_existing_card(&self, input: pb::RenderExistingCardIn) -> Result { self.with_col(|col| { col.render_existing_card(CardID(input.card_id), input.browser) .map(Into::into) @@ -206,7 +196,7 @@ impl BackendService for Backend { fn render_uncommitted_card( &self, input: pb::RenderUncommittedCardIn, - ) -> BackendResult { + ) -> Result { let schema11: CardTemplateSchema11 = serde_json::from_slice(&input.template)?; let template = schema11.into(); let mut note = input @@ -221,7 +211,7 @@ impl BackendService for Backend { }) } - fn strip_av_tags(&self, input: pb::String) -> BackendResult { + fn strip_av_tags(&self, input: pb::String) -> Result { Ok(pb::String { val: strip_av_tags(&input.val).into(), }) @@ -277,7 +267,7 @@ impl BackendService for Backend { Ok(replace_search_node(existing, replacement).into()) } - fn find_and_replace(&self, input: pb::FindAndReplaceIn) -> BackendResult { + fn find_and_replace(&self, input: pb::FindAndReplaceIn) -> Result { let mut search = if input.regex { input.search } else { @@ -298,187 +288,23 @@ impl BackendService for Backend { .map(|cnt| pb::UInt32 { val: cnt as u32 }) }) } - - // scheduling - //----------------------------------------------- - - /// This behaves like _updateCutoff() in older code - it also unburies at the start of - /// a new day. - fn sched_timing_today(&self, _input: pb::Empty) -> Result { - self.with_col(|col| { - let timing = col.timing_today()?; - col.unbury_if_day_rolled_over(timing)?; - Ok(timing.into()) - }) - } - - /// Fetch data from DB and return rendered string. - fn studied_today(&self, _input: pb::Empty) -> BackendResult { - self.with_col(|col| col.studied_today().map(Into::into)) - } - - /// Message rendering only, for old graphs. - fn studied_today_message(&self, input: pb::StudiedTodayMessageIn) -> BackendResult { - Ok(studied_today(input.cards, input.seconds as f32, &self.i18n).into()) - } - - fn update_stats(&self, input: pb::UpdateStatsIn) -> BackendResult { - self.with_col(|col| { - col.transact(None, |col| { - let today = col.current_due_day(0)?; - let usn = col.usn()?; - col.update_deck_stats(today, usn, input).map(Into::into) - }) - }) - } - - fn extend_limits(&self, input: pb::ExtendLimitsIn) -> BackendResult { - self.with_col(|col| { - col.transact(None, |col| { - let today = col.current_due_day(0)?; - let usn = col.usn()?; - col.extend_limits( - today, - usn, - input.deck_id.into(), - input.new_delta, - input.review_delta, - ) - .map(Into::into) - }) - }) - } - - fn counts_for_deck_today(&self, input: pb::DeckId) -> BackendResult { - self.with_col(|col| col.counts_for_deck_today(input.did.into())) - } - - fn congrats_info(&self, _input: Empty) -> BackendResult { - self.with_col(|col| col.congrats_info()) - } - - fn restore_buried_and_suspended_cards(&self, input: pb::CardIDs) -> BackendResult { - let cids: Vec<_> = input.into(); - self.with_col(|col| col.unbury_or_unsuspend_cards(&cids).map(Into::into)) - } - - fn unbury_cards_in_current_deck( - &self, - input: pb::UnburyCardsInCurrentDeckIn, - ) -> BackendResult { - self.with_col(|col| { - col.unbury_cards_in_current_deck(input.mode()) - .map(Into::into) - }) - } - - fn bury_or_suspend_cards(&self, input: pb::BuryOrSuspendCardsIn) -> BackendResult { - self.with_col(|col| { - let mode = input.mode(); - let cids: Vec<_> = input.card_ids.into_iter().map(CardID).collect(); - col.bury_or_suspend_cards(&cids, mode).map(Into::into) - }) - } - - fn empty_filtered_deck(&self, input: pb::DeckId) -> BackendResult { - self.with_col(|col| col.empty_filtered_deck(input.did.into()).map(Into::into)) - } - - fn rebuild_filtered_deck(&self, input: pb::DeckId) -> BackendResult { - self.with_col(|col| col.rebuild_filtered_deck(input.did.into()).map(Into::into)) - } - - fn schedule_cards_as_new(&self, input: pb::ScheduleCardsAsNewIn) -> BackendResult { - self.with_col(|col| { - let cids: Vec<_> = input.card_ids.into_iter().map(CardID).collect(); - let log = input.log; - col.reschedule_cards_as_new(&cids, log).map(Into::into) - }) - } - - fn set_due_date(&self, input: pb::SetDueDateIn) -> BackendResult { - let cids: Vec<_> = input.card_ids.into_iter().map(CardID).collect(); - let spec = parse_due_date_str(&input.days)?; - self.with_col(|col| col.set_due_date(&cids, spec).map(Into::into)) - } - - fn sort_cards(&self, input: pb::SortCardsIn) -> BackendResult { - let cids: Vec<_> = input.card_ids.into_iter().map(CardID).collect(); - let (start, step, random, shift) = ( - input.starting_from, - input.step_size, - input.randomize, - input.shift_existing, - ); - let order = if random { - NewCardSortOrder::Random - } else { - NewCardSortOrder::Preserve - }; - self.with_col(|col| { - col.sort_cards(&cids, start, step, order, shift) - .map(Into::into) - }) - } - - fn sort_deck(&self, input: pb::SortDeckIn) -> BackendResult { - self.with_col(|col| { - col.sort_deck(input.deck_id.into(), input.randomize) - .map(Into::into) - }) - } - - fn get_next_card_states(&self, input: pb::CardId) -> BackendResult { - let cid: CardID = input.into(); - self.with_col(|col| col.get_next_card_states(cid)) - .map(Into::into) - } - - fn describe_next_states(&self, input: pb::NextCardStates) -> BackendResult { - let states: NextCardStates = input.into(); - self.with_col(|col| col.describe_next_states(states)) - .map(Into::into) - } - - fn state_is_leech(&self, input: pb::SchedulingState) -> BackendResult { - let state: CardState = input.into(); - Ok(state.leeched().into()) - } - - fn answer_card(&self, input: pb::AnswerCardIn) -> BackendResult { - self.with_col(|col| col.answer_card(&input.into())) - .map(Into::into) - } - - fn upgrade_scheduler(&self, _input: Empty) -> BackendResult { - self.with_col(|col| col.transact(None, |col| col.upgrade_to_v2_scheduler())) - .map(Into::into) - } - - fn get_queued_cards( - &self, - input: pb::GetQueuedCardsIn, - ) -> BackendResult { - self.with_col(|col| col.get_queued_cards(input.fetch_limit, input.intraday_learning_only)) - } - // statistics //----------------------------------------------- - fn card_stats(&self, input: pb::CardId) -> BackendResult { + fn card_stats(&self, input: pb::CardId) -> Result { self.with_col(|col| col.card_stats(input.into())) .map(Into::into) } - fn graphs(&self, input: pb::GraphsIn) -> BackendResult { + fn graphs(&self, input: pb::GraphsIn) -> Result { self.with_col(|col| col.graph_data_for_search(&input.search, input.days)) } - fn get_graph_preferences(&self, _input: pb::Empty) -> BackendResult { + fn get_graph_preferences(&self, _input: pb::Empty) -> Result { self.with_col(|col| col.get_graph_preferences()) } - fn set_graph_preferences(&self, input: pb::GraphPreferences) -> BackendResult { + fn set_graph_preferences(&self, input: pb::GraphPreferences) -> Result { self.with_col(|col| col.set_graph_preferences(input)) .map(Into::into) } @@ -508,7 +334,7 @@ impl BackendService for Backend { }) } - fn trash_media_files(&self, input: pb::TrashMediaFilesIn) -> BackendResult { + fn trash_media_files(&self, input: pb::TrashMediaFilesIn) -> Result { self.with_col(|col| { let mgr = MediaManager::new(&col.media_folder, &col.media_db)?; let mut ctx = mgr.dbctx(); @@ -517,7 +343,7 @@ impl BackendService for Backend { .map(Into::into) } - fn add_media_file(&self, input: pb::AddMediaFileIn) -> BackendResult { + fn add_media_file(&self, input: pb::AddMediaFileIn) -> Result { self.with_col(|col| { let mgr = MediaManager::new(&col.media_folder, &col.media_db)?; let mut ctx = mgr.dbctx(); @@ -528,7 +354,7 @@ impl BackendService for Backend { }) } - fn empty_trash(&self, _input: Empty) -> BackendResult { + fn empty_trash(&self, _input: Empty) -> Result { let mut handler = self.new_progress_handler(); let progress_fn = move |progress| handler.update(Progress::MediaCheck(progress as u32), true); @@ -544,7 +370,7 @@ impl BackendService for Backend { .map(Into::into) } - fn restore_trash(&self, _input: Empty) -> BackendResult { + fn restore_trash(&self, _input: Empty) -> Result { let mut handler = self.new_progress_handler(); let progress_fn = move |progress| handler.update(Progress::MediaCheck(progress as u32), true); @@ -595,7 +421,7 @@ impl BackendService for Backend { }) } - fn deck_tree_legacy(&self, _input: pb::Empty) -> BackendResult { + fn deck_tree_legacy(&self, _input: pb::Empty) -> Result { self.with_col(|col| { let tree = col.legacy_deck_tree()?; serde_json::to_vec(&tree) @@ -604,7 +430,7 @@ impl BackendService for Backend { }) } - fn get_all_decks_legacy(&self, _input: Empty) -> BackendResult { + fn get_all_decks_legacy(&self, _input: Empty) -> Result { self.with_col(|col| { let decks = col.storage.get_all_decks_as_schema11()?; serde_json::to_vec(&decks).map_err(Into::into) @@ -650,7 +476,7 @@ impl BackendService for Backend { }) } - fn new_deck_legacy(&self, input: pb::Bool) -> BackendResult { + fn new_deck_legacy(&self, input: pb::Bool) -> Result { let deck = if input.val { Deck::new_filtered() } else { @@ -662,12 +488,12 @@ impl BackendService for Backend { .map(Into::into) } - fn remove_deck(&self, input: pb::DeckId) -> BackendResult { + fn remove_deck(&self, input: pb::DeckId) -> Result { self.with_col(|col| col.remove_deck_and_child_decks(input.into())) .map(Into::into) } - fn drag_drop_decks(&self, input: pb::DragDropDecksIn) -> BackendResult { + fn drag_drop_decks(&self, input: pb::DragDropDecksIn) -> Result { let source_dids: Vec<_> = input.source_deck_ids.into_iter().map(Into::into).collect(); let target_did = if input.target_deck_id == 0 { None @@ -684,7 +510,7 @@ impl BackendService for Backend { fn add_or_update_deck_config_legacy( &self, input: AddOrUpdateDeckConfigLegacyIn, - ) -> BackendResult { + ) -> Result { let conf: DeckConfSchema11 = serde_json::from_slice(&input.config)?; let mut conf: DeckConf = conf.into(); self.with_col(|col| { @@ -696,7 +522,7 @@ impl BackendService for Backend { .map(Into::into) } - fn all_deck_config_legacy(&self, _input: Empty) -> BackendResult { + fn all_deck_config_legacy(&self, _input: Empty) -> Result { self.with_col(|col| { let conf: Vec = col .storage @@ -709,7 +535,7 @@ impl BackendService for Backend { .map(Into::into) } - fn get_deck_config_legacy(&self, input: pb::DeckConfigId) -> BackendResult { + fn get_deck_config_legacy(&self, input: pb::DeckConfigId) -> Result { self.with_col(|col| { let conf = col.get_deck_config(input.into(), true)?.unwrap(); let conf: DeckConfSchema11 = conf.into(); @@ -718,13 +544,13 @@ impl BackendService for Backend { .map(Into::into) } - fn new_deck_config_legacy(&self, _input: Empty) -> BackendResult { + fn new_deck_config_legacy(&self, _input: Empty) -> Result { serde_json::to_vec(&DeckConfSchema11::default()) .map_err(Into::into) .map(Into::into) } - fn remove_deck_config(&self, input: pb::DeckConfigId) -> BackendResult { + fn remove_deck_config(&self, input: pb::DeckConfigId) -> Result { self.with_col(|col| col.transact(None, |col| col.remove_deck_config(input.into()))) .map(Into::into) } @@ -732,7 +558,7 @@ impl BackendService for Backend { // cards //------------------------------------------------------------------- - fn get_card(&self, input: pb::CardId) -> BackendResult { + fn get_card(&self, input: pb::CardId) -> Result { self.with_col(|col| { col.storage .get_card(input.into()) @@ -741,7 +567,7 @@ impl BackendService for Backend { }) } - fn update_card(&self, input: pb::UpdateCardIn) -> BackendResult { + fn update_card(&self, input: pb::UpdateCardIn) -> Result { self.with_col(|col| { let op = if input.skip_undo_entry { None @@ -754,7 +580,7 @@ impl BackendService for Backend { .map(Into::into) } - fn remove_cards(&self, input: pb::RemoveCardsIn) -> BackendResult { + fn remove_cards(&self, input: pb::RemoveCardsIn) -> Result { self.with_col(|col| { col.transact(None, |col| { col.remove_cards_and_orphaned_notes( @@ -769,7 +595,7 @@ impl BackendService for Backend { }) } - fn set_deck(&self, input: pb::SetDeckIn) -> BackendResult { + fn set_deck(&self, input: pb::SetDeckIn) -> Result { let cids: Vec<_> = input.card_ids.into_iter().map(CardID).collect(); let deck_id = input.deck_id.into(); self.with_col(|col| col.set_deck(&cids, deck_id).map(Into::into)) @@ -778,14 +604,14 @@ impl BackendService for Backend { // notes //------------------------------------------------------------------- - fn new_note(&self, input: pb::NoteTypeId) -> BackendResult { + fn new_note(&self, input: pb::NoteTypeId) -> Result { self.with_col(|col| { let nt = col.get_notetype(input.into())?.ok_or(AnkiError::NotFound)?; Ok(nt.new_note().into()) }) } - fn add_note(&self, input: pb::AddNoteIn) -> BackendResult { + fn add_note(&self, input: pb::AddNoteIn) -> Result { self.with_col(|col| { let mut note: Note = input.note.ok_or(AnkiError::NotFound)?.into(); col.add_note(&mut note, DeckID(input.deck_id)) @@ -793,17 +619,14 @@ impl BackendService for Backend { }) } - fn defaults_for_adding( - &self, - input: pb::DefaultsForAddingIn, - ) -> BackendResult { + fn defaults_for_adding(&self, input: pb::DefaultsForAddingIn) -> Result { self.with_col(|col| { let home_deck: DeckID = input.home_deck_of_current_review_card.into(); col.defaults_for_adding(home_deck).map(Into::into) }) } - fn default_deck_for_notetype(&self, input: pb::NoteTypeId) -> BackendResult { + fn default_deck_for_notetype(&self, input: pb::NoteTypeId) -> Result { self.with_col(|col| { Ok(col .default_deck_for_notetype(input.into())? @@ -812,7 +635,7 @@ impl BackendService for Backend { }) } - fn update_note(&self, input: pb::UpdateNoteIn) -> BackendResult { + fn update_note(&self, input: pb::UpdateNoteIn) -> Result { self.with_col(|col| { let op = if input.skip_undo_entry { None @@ -825,7 +648,7 @@ impl BackendService for Backend { .map(Into::into) } - fn get_note(&self, input: pb::NoteId) -> BackendResult { + fn get_note(&self, input: pb::NoteId) -> Result { self.with_col(|col| { col.storage .get_note(input.into())? @@ -834,7 +657,7 @@ impl BackendService for Backend { }) } - fn remove_notes(&self, input: pb::RemoveNotesIn) -> BackendResult { + fn remove_notes(&self, input: pb::RemoveNotesIn) -> Result { self.with_col(|col| { if !input.note_ids.is_empty() { col.remove_notes( @@ -859,7 +682,7 @@ impl BackendService for Backend { }) } - fn add_note_tags(&self, input: pb::AddNoteTagsIn) -> BackendResult { + fn add_note_tags(&self, input: pb::AddNoteTagsIn) -> Result { self.with_col(|col| { col.add_tags_to_notes(&to_nids(input.nids), &input.tags) .map(|n| n as u32) @@ -867,7 +690,7 @@ impl BackendService for Backend { .map(Into::into) } - fn update_note_tags(&self, input: pb::UpdateNoteTagsIn) -> BackendResult { + fn update_note_tags(&self, input: pb::UpdateNoteTagsIn) -> Result { self.with_col(|col| { col.replace_tags_for_notes( &to_nids(input.nids), @@ -879,7 +702,7 @@ impl BackendService for Backend { }) } - fn cloze_numbers_in_note(&self, note: pb::Note) -> BackendResult { + fn cloze_numbers_in_note(&self, note: pb::Note) -> Result { let mut set = HashSet::with_capacity(4); for field in ¬e.fields { add_cloze_numbers_in_string(field, &mut set); @@ -889,7 +712,7 @@ impl BackendService for Backend { }) } - fn after_note_updates(&self, input: pb::AfterNoteUpdatesIn) -> BackendResult { + fn after_note_updates(&self, input: pb::AfterNoteUpdatesIn) -> Result { self.with_col(|col| { col.transact(None, |col| { col.after_note_updates( @@ -905,7 +728,7 @@ impl BackendService for Backend { fn field_names_for_notes( &self, input: pb::FieldNamesForNotesIn, - ) -> BackendResult { + ) -> Result { self.with_col(|col| { let nids: Vec<_> = input.nids.into_iter().map(NoteID).collect(); col.storage @@ -914,10 +737,7 @@ impl BackendService for Backend { }) } - fn note_is_duplicate_or_empty( - &self, - input: pb::Note, - ) -> BackendResult { + fn note_is_duplicate_or_empty(&self, input: pb::Note) -> Result { let note: Note = input.into(); self.with_col(|col| { col.note_is_duplicate_or_empty(¬e) @@ -925,7 +745,7 @@ impl BackendService for Backend { }) } - fn cards_of_note(&self, input: pb::NoteId) -> BackendResult { + fn cards_of_note(&self, input: pb::NoteId) -> Result { self.with_col(|col| { col.storage .all_card_ids_of_note(NoteID(input.nid)) @@ -938,10 +758,7 @@ impl BackendService for Backend { // notetypes //------------------------------------------------------------------- - fn add_or_update_notetype( - &self, - input: pb::AddOrUpdateNotetypeIn, - ) -> BackendResult { + fn add_or_update_notetype(&self, input: pb::AddOrUpdateNotetypeIn) -> Result { self.with_col(|col| { let legacy: NoteTypeSchema11 = serde_json::from_slice(&input.json)?; let mut nt: NoteType = legacy.into(); @@ -954,7 +771,7 @@ impl BackendService for Backend { }) } - fn get_stock_notetype_legacy(&self, input: pb::StockNoteType) -> BackendResult { + fn get_stock_notetype_legacy(&self, input: pb::StockNoteType) -> Result { // fixme: use individual functions instead of full vec let mut all = all_stock_notetypes(&self.i18n); let idx = (input.kind as usize).min(all.len() - 1); @@ -965,7 +782,7 @@ impl BackendService for Backend { .map(Into::into) } - fn get_notetype_legacy(&self, input: pb::NoteTypeId) -> BackendResult { + fn get_notetype_legacy(&self, input: pb::NoteTypeId) -> Result { self.with_col(|col| { let schema11: NoteTypeSchema11 = col .storage @@ -976,7 +793,7 @@ impl BackendService for Backend { }) } - fn get_notetype_names(&self, _input: Empty) -> BackendResult { + fn get_notetype_names(&self, _input: Empty) -> Result { self.with_col(|col| { let entries: Vec<_> = col .storage @@ -988,7 +805,7 @@ impl BackendService for Backend { }) } - fn get_notetype_names_and_counts(&self, _input: Empty) -> BackendResult { + fn get_notetype_names_and_counts(&self, _input: Empty) -> Result { self.with_col(|col| { let entries: Vec<_> = col .storage @@ -1004,7 +821,7 @@ impl BackendService for Backend { }) } - fn get_notetype_id_by_name(&self, input: pb::String) -> BackendResult { + fn get_notetype_id_by_name(&self, input: pb::String) -> Result { self.with_col(|col| { col.storage .get_notetype_id(&input.val) @@ -1013,7 +830,7 @@ impl BackendService for Backend { }) } - fn remove_notetype(&self, input: pb::NoteTypeId) -> BackendResult { + fn remove_notetype(&self, input: pb::NoteTypeId) -> Result { self.with_col(|col| col.remove_notetype(input.into())) .map(Into::into) } @@ -1021,7 +838,7 @@ impl BackendService for Backend { // collection //------------------------------------------------------------------- - fn open_collection(&self, input: pb::OpenCollectionIn) -> BackendResult { + fn open_collection(&self, input: pb::OpenCollectionIn) -> Result { let mut col = self.col.lock().unwrap(); if col.is_some() { return Err(AnkiError::CollectionAlreadyOpen); @@ -1050,7 +867,7 @@ impl BackendService for Backend { Ok(().into()) } - fn close_collection(&self, input: pb::CloseCollectionIn) -> BackendResult { + fn close_collection(&self, input: pb::CloseCollectionIn) -> Result { self.abort_media_sync_and_wait(); let mut col = self.col.lock().unwrap(); @@ -1069,7 +886,7 @@ impl BackendService for Backend { Ok(().into()) } - fn check_database(&self, _input: pb::Empty) -> BackendResult { + fn check_database(&self, _input: pb::Empty) -> Result { let mut handler = self.new_progress_handler(); let progress_fn = move |progress, throttle| { handler.update(Progress::DatabaseCheck(progress), throttle); @@ -1103,11 +920,11 @@ impl BackendService for Backend { // sync //------------------------------------------------------------------- - fn sync_media(&self, input: pb::SyncAuth) -> BackendResult { + fn sync_media(&self, input: pb::SyncAuth) -> Result { self.sync_media_inner(input).map(Into::into) } - fn abort_sync(&self, _input: Empty) -> BackendResult { + fn abort_sync(&self, _input: Empty) -> Result { if let Some(handle) = self.sync_abort.lock().unwrap().take() { handle.abort(); } @@ -1115,7 +932,7 @@ impl BackendService for Backend { } /// Abort the media sync. Does not wait for completion. - fn abort_media_sync(&self, _input: Empty) -> BackendResult { + fn abort_media_sync(&self, _input: Empty) -> Result { let guard = self.state.lock().unwrap(); if let Some(handle) = &guard.media_sync_abort { handle.abort(); @@ -1123,33 +940,33 @@ impl BackendService for Backend { Ok(().into()) } - fn before_upload(&self, _input: Empty) -> BackendResult { + fn before_upload(&self, _input: Empty) -> Result { self.with_col(|col| col.before_upload().map(Into::into)) } - fn sync_login(&self, input: pb::SyncLoginIn) -> BackendResult { + fn sync_login(&self, input: pb::SyncLoginIn) -> Result { self.sync_login_inner(input) } - fn sync_status(&self, input: pb::SyncAuth) -> BackendResult { + fn sync_status(&self, input: pb::SyncAuth) -> Result { self.sync_status_inner(input) } - fn sync_collection(&self, input: pb::SyncAuth) -> BackendResult { + fn sync_collection(&self, input: pb::SyncAuth) -> Result { self.sync_collection_inner(input) } - fn full_upload(&self, input: pb::SyncAuth) -> BackendResult { + fn full_upload(&self, input: pb::SyncAuth) -> Result { self.full_sync_inner(input, true)?; Ok(().into()) } - fn full_download(&self, input: pb::SyncAuth) -> BackendResult { + fn full_download(&self, input: pb::SyncAuth) -> Result { self.full_sync_inner(input, false)?; Ok(().into()) } - fn sync_server_method(&self, input: pb::SyncServerMethodIn) -> BackendResult { + fn sync_server_method(&self, input: pb::SyncServerMethodIn) -> Result { let req = SyncRequest::from_method_and_data(input.method(), input.data)?; self.sync_server_method_inner(req).map(Into::into) } @@ -1157,7 +974,7 @@ impl BackendService for Backend { // i18n/messages //------------------------------------------------------------------- - fn translate_string(&self, input: pb::TranslateStringIn) -> BackendResult { + fn translate_string(&self, input: pb::TranslateStringIn) -> Result { let key = match crate::fluent_proto::FluentString::from_i32(input.key) { Some(key) => key, None => return Ok("invalid key".to_string().into()), @@ -1172,7 +989,7 @@ impl BackendService for Backend { Ok(self.i18n.trn(key, map).into()) } - fn format_timespan(&self, input: pb::FormatTimespanIn) -> BackendResult { + fn format_timespan(&self, input: pb::FormatTimespanIn) -> Result { use pb::format_timespan_in::Context; Ok(match input.context() { Context::Precise => time_span(input.seconds, &self.i18n, true), @@ -1182,13 +999,13 @@ impl BackendService for Backend { .into()) } - fn i18n_resources(&self, _input: Empty) -> BackendResult { + fn i18n_resources(&self, _input: Empty) -> Result { serde_json::to_vec(&self.i18n.resources_for_js()) .map(Into::into) .map_err(Into::into) } - fn render_markdown(&self, input: pb::RenderMarkdownIn) -> BackendResult { + fn render_markdown(&self, input: pb::RenderMarkdownIn) -> Result { let mut text = render_markdown(&input.markdown); if input.sanitize { // currently no images @@ -1200,11 +1017,11 @@ impl BackendService for Backend { // tags //------------------------------------------------------------------- - fn clear_unused_tags(&self, _input: pb::Empty) -> BackendResult { + fn clear_unused_tags(&self, _input: pb::Empty) -> Result { self.with_col(|col| col.transact(None, |col| col.clear_unused_tags().map(Into::into))) } - fn all_tags(&self, _input: Empty) -> BackendResult { + fn all_tags(&self, _input: Empty) -> Result { Ok(pb::StringList { vals: self.with_col(|col| { Ok(col @@ -1217,7 +1034,7 @@ impl BackendService for Backend { }) } - fn set_tag_expanded(&self, input: pb::SetTagExpandedIn) -> BackendResult { + fn set_tag_expanded(&self, input: pb::SetTagExpandedIn) -> Result { self.with_col(|col| { col.transact(None, |col| { col.set_tag_expanded(&input.name, input.expanded)?; @@ -1226,7 +1043,7 @@ impl BackendService for Backend { }) } - fn clear_tag(&self, tag: pb::String) -> BackendResult { + fn clear_tag(&self, tag: pb::String) -> Result { self.with_col(|col| { col.transact(None, |col| { col.storage.clear_tag_and_children(tag.val.as_str())?; @@ -1239,7 +1056,7 @@ impl BackendService for Backend { self.with_col(|col| col.tag_tree()) } - fn drag_drop_tags(&self, input: pb::DragDropTagsIn) -> BackendResult { + fn drag_drop_tags(&self, input: pb::DragDropTagsIn) -> Result { let source_tags = input.source_tags; let target_tag = if input.target_tag.is_empty() { None @@ -1253,7 +1070,7 @@ impl BackendService for Backend { // config/preferences //------------------------------------------------------------------- - fn get_config_json(&self, input: pb::String) -> BackendResult { + fn get_config_json(&self, input: pb::String) -> Result { self.with_col(|col| { let val: Option = col.get_config_optional(input.val.as_str()); val.ok_or(AnkiError::NotFound) @@ -1262,7 +1079,7 @@ impl BackendService for Backend { }) } - fn set_config_json(&self, input: pb::SetConfigJsonIn) -> BackendResult { + fn set_config_json(&self, input: pb::SetConfigJsonIn) -> Result { self.with_col(|col| { col.transact(None, |col| { // ensure it's a well-formed object @@ -1273,12 +1090,12 @@ impl BackendService for Backend { .map(Into::into) } - fn remove_config(&self, input: pb::String) -> BackendResult { + fn remove_config(&self, input: pb::String) -> Result { self.with_col(|col| col.transact(None, |col| col.remove_config(input.val.as_str()))) .map(Into::into) } - fn get_all_config(&self, _input: Empty) -> BackendResult { + fn get_all_config(&self, _input: Empty) -> Result { self.with_col(|col| { let conf = col.storage.get_all_config()?; serde_json::to_vec(&conf).map_err(Into::into) @@ -1286,7 +1103,7 @@ impl BackendService for Backend { .map(Into::into) } - fn get_config_bool(&self, input: pb::config::Bool) -> BackendResult { + fn get_config_bool(&self, input: pb::config::Bool) -> Result { self.with_col(|col| { Ok(pb::Bool { val: col.get_bool(input.key().into()), @@ -1294,12 +1111,12 @@ impl BackendService for Backend { }) } - fn set_config_bool(&self, input: pb::SetConfigBoolIn) -> BackendResult { + fn set_config_bool(&self, input: pb::SetConfigBoolIn) -> Result { self.with_col(|col| col.transact(None, |col| col.set_bool(input.key().into(), input.value))) .map(Into::into) } - fn get_config_string(&self, input: pb::config::String) -> BackendResult { + fn get_config_string(&self, input: pb::config::String) -> Result { self.with_col(|col| { Ok(pb::String { val: col.get_string(input.key().into()), @@ -1307,18 +1124,18 @@ impl BackendService for Backend { }) } - fn set_config_string(&self, input: pb::SetConfigStringIn) -> BackendResult { + fn set_config_string(&self, input: pb::SetConfigStringIn) -> Result { self.with_col(|col| { col.transact(None, |col| col.set_string(input.key().into(), &input.value)) }) .map(Into::into) } - fn get_preferences(&self, _input: Empty) -> BackendResult { + fn get_preferences(&self, _input: Empty) -> Result { self.with_col(|col| col.get_preferences()) } - fn set_preferences(&self, input: pb::Preferences) -> BackendResult { + fn set_preferences(&self, input: pb::Preferences) -> Result { self.with_col(|col| col.set_preferences(input)) .map(Into::into) } @@ -1344,13 +1161,24 @@ impl Backend { &self.i18n } - pub fn run_command_bytes(&self, method: u32, input: &[u8]) -> result::Result, Vec> { - self.run_command_bytes2_inner(method, input).map_err(|err| { - let backend_err = anki_error_to_proto_error(err, &self.i18n); - let mut bytes = Vec::new(); - backend_err.encode(&mut bytes).unwrap(); - bytes - }) + pub fn run_method( + &self, + service: u32, + method: u32, + input: &[u8], + ) -> result::Result, Vec> { + pb::ServiceIndex::from_i32(service as i32) + .ok_or_else(|| AnkiError::invalid_input("invalid service")) + .and_then(|service| match service { + pb::ServiceIndex::Scheduling => SchedulingService::run_method(self, method, input), + pb::ServiceIndex::Backend => BackendService::run_method(self, method, input), + }) + .map_err(|err| { + let backend_err = anki_error_to_proto_error(err, &self.i18n); + let mut bytes = Vec::new(); + backend_err.encode(&mut bytes).unwrap(); + bytes + }) } /// If collection is open, run the provided closure while holding diff --git a/rslib/src/backend/scheduler/mod.rs b/rslib/src/backend/scheduler/mod.rs index 8663da96c..73b0b1536 100644 --- a/rslib/src/backend/scheduler/mod.rs +++ b/rslib/src/backend/scheduler/mod.rs @@ -3,3 +3,175 @@ mod answering; mod states; + +use super::Backend; +use crate::{ + backend_proto::{self as pb}, + prelude::*, + scheduler::{ + new::NewCardSortOrder, + parse_due_date_str, + states::{CardState, NextCardStates}, + }, + stats::studied_today, +}; +pub(super) use pb::scheduling::Service as SchedulingService; + +impl SchedulingService for Backend { + /// This behaves like _updateCutoff() in older code - it also unburies at the start of + /// a new day. + fn sched_timing_today(&self, _input: pb::Empty) -> Result { + self.with_col(|col| { + let timing = col.timing_today()?; + col.unbury_if_day_rolled_over(timing)?; + Ok(timing.into()) + }) + } + + /// Fetch data from DB and return rendered string. + fn studied_today(&self, _input: pb::Empty) -> Result { + self.with_col(|col| col.studied_today().map(Into::into)) + } + + /// Message rendering only, for old graphs. + fn studied_today_message(&self, input: pb::StudiedTodayMessageIn) -> Result { + Ok(studied_today(input.cards, input.seconds as f32, &self.i18n).into()) + } + + fn update_stats(&self, input: pb::UpdateStatsIn) -> Result { + self.with_col(|col| { + col.transact(None, |col| { + let today = col.current_due_day(0)?; + let usn = col.usn()?; + col.update_deck_stats(today, usn, input).map(Into::into) + }) + }) + } + + fn extend_limits(&self, input: pb::ExtendLimitsIn) -> Result { + self.with_col(|col| { + col.transact(None, |col| { + let today = col.current_due_day(0)?; + let usn = col.usn()?; + col.extend_limits( + today, + usn, + input.deck_id.into(), + input.new_delta, + input.review_delta, + ) + .map(Into::into) + }) + }) + } + + fn counts_for_deck_today(&self, input: pb::DeckId) -> Result { + self.with_col(|col| col.counts_for_deck_today(input.did.into())) + } + + fn congrats_info(&self, _input: pb::Empty) -> Result { + self.with_col(|col| col.congrats_info()) + } + + fn restore_buried_and_suspended_cards(&self, input: pb::CardIDs) -> Result { + let cids: Vec<_> = input.into(); + self.with_col(|col| col.unbury_or_unsuspend_cards(&cids).map(Into::into)) + } + + fn unbury_cards_in_current_deck( + &self, + input: pb::UnburyCardsInCurrentDeckIn, + ) -> Result { + self.with_col(|col| { + col.unbury_cards_in_current_deck(input.mode()) + .map(Into::into) + }) + } + + fn bury_or_suspend_cards(&self, input: pb::BuryOrSuspendCardsIn) -> Result { + self.with_col(|col| { + let mode = input.mode(); + let cids: Vec<_> = input.card_ids.into_iter().map(CardID).collect(); + col.bury_or_suspend_cards(&cids, mode).map(Into::into) + }) + } + + fn empty_filtered_deck(&self, input: pb::DeckId) -> Result { + self.with_col(|col| col.empty_filtered_deck(input.did.into()).map(Into::into)) + } + + fn rebuild_filtered_deck(&self, input: pb::DeckId) -> Result { + self.with_col(|col| col.rebuild_filtered_deck(input.did.into()).map(Into::into)) + } + + fn schedule_cards_as_new(&self, input: pb::ScheduleCardsAsNewIn) -> Result { + self.with_col(|col| { + let cids: Vec<_> = input.card_ids.into_iter().map(CardID).collect(); + let log = input.log; + col.reschedule_cards_as_new(&cids, log).map(Into::into) + }) + } + + fn set_due_date(&self, input: pb::SetDueDateIn) -> Result { + let cids: Vec<_> = input.card_ids.into_iter().map(CardID).collect(); + let spec = parse_due_date_str(&input.days)?; + self.with_col(|col| col.set_due_date(&cids, spec).map(Into::into)) + } + + fn sort_cards(&self, input: pb::SortCardsIn) -> Result { + let cids: Vec<_> = input.card_ids.into_iter().map(CardID).collect(); + let (start, step, random, shift) = ( + input.starting_from, + input.step_size, + input.randomize, + input.shift_existing, + ); + let order = if random { + NewCardSortOrder::Random + } else { + NewCardSortOrder::Preserve + }; + self.with_col(|col| { + col.sort_cards(&cids, start, step, order, shift) + .map(Into::into) + }) + } + + fn sort_deck(&self, input: pb::SortDeckIn) -> Result { + self.with_col(|col| { + col.sort_deck(input.deck_id.into(), input.randomize) + .map(Into::into) + }) + } + + fn get_next_card_states(&self, input: pb::CardId) -> Result { + let cid: CardID = input.into(); + self.with_col(|col| col.get_next_card_states(cid)) + .map(Into::into) + } + + fn describe_next_states(&self, input: pb::NextCardStates) -> Result { + let states: NextCardStates = input.into(); + self.with_col(|col| col.describe_next_states(states)) + .map(Into::into) + } + + fn state_is_leech(&self, input: pb::SchedulingState) -> Result { + let state: CardState = input.into(); + Ok(state.leeched().into()) + } + + fn answer_card(&self, input: pb::AnswerCardIn) -> Result { + self.with_col(|col| col.answer_card(&input.into())) + .map(Into::into) + } + + fn upgrade_scheduler(&self, _input: pb::Empty) -> Result { + self.with_col(|col| col.transact(None, |col| col.upgrade_to_v2_scheduler())) + .map(Into::into) + } + + fn get_queued_cards(&self, input: pb::GetQueuedCardsIn) -> Result { + self.with_col(|col| col.get_queued_cards(input.fetch_limit, input.intraday_learning_only)) + } +} From 1b8d6c6e85025ef0b6f102d3e7e857ab137707d9 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 11 Mar 2021 15:47:31 +1000 Subject: [PATCH 08/13] split out sync, notetypes and config code --- rslib/backend.proto | 142 ++--- rslib/build/protobuf.rs | 2 +- rslib/src/backend/config.rs | 76 +++ rslib/src/backend/decks.rs | 127 ++++ rslib/src/backend/mod.rs | 560 ++---------------- rslib/src/backend/notes.rs | 172 ++++++ rslib/src/backend/notetypes.rs | 89 +++ rslib/src/backend/scheduler/mod.rs | 2 +- rslib/src/backend/{sync.rs => sync/mod.rs} | 89 ++- .../{http_sync_server.rs => sync/server.rs} | 21 +- 10 files changed, 664 insertions(+), 616 deletions(-) create mode 100644 rslib/src/backend/decks.rs create mode 100644 rslib/src/backend/notes.rs create mode 100644 rslib/src/backend/notetypes.rs rename rslib/src/backend/{sync.rs => sync/mod.rs} (78%) rename rslib/src/backend/{http_sync_server.rs => sync/server.rs} (94%) diff --git a/rslib/backend.proto b/rslib/backend.proto index 11a390895..a7b02f105 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -80,7 +80,12 @@ message DeckConfigID { /// sure all clients agree on the same service indices enum ServiceIndex { SERVICE_INDEX_SCHEDULING = 0; - SERVICE_INDEX_BACKEND = 1; + SERVICE_INDEX_DECKS = 1; + SERVICE_INDEX_NOTES = 2; + SERVICE_INDEX_SYNC = 3; + SERVICE_INDEX_NOTETYPES = 4; + SERVICE_INDEX_CONFIG = 5; + SERVICE_INDEX_BACKEND = 99; } service SchedulingService { @@ -108,6 +113,72 @@ service SchedulingService { rpc GetQueuedCards(GetQueuedCardsIn) returns (GetQueuedCardsOut); } +service DecksService { + rpc AddOrUpdateDeckLegacy(AddOrUpdateDeckLegacyIn) returns (DeckID); + rpc DeckTree(DeckTreeIn) returns (DeckTreeNode); + rpc DeckTreeLegacy(Empty) returns (Json); + rpc GetAllDecksLegacy(Empty) returns (Json); + rpc GetDeckIDByName(String) returns (DeckID); + rpc GetDeckLegacy(DeckID) returns (Json); + rpc GetDeckNames(GetDeckNamesIn) returns (DeckNames); + rpc NewDeckLegacy(Bool) returns (Json); + rpc RemoveDeck(DeckID) returns (Empty); + rpc DragDropDecks(DragDropDecksIn) returns (Empty); +} + +service NotesService { + rpc NewNote(NoteTypeID) returns (Note); + rpc AddNote(AddNoteIn) returns (NoteID); + rpc DefaultsForAdding(DefaultsForAddingIn) returns (DeckAndNotetype); + rpc DefaultDeckForNotetype(NoteTypeID) returns (DeckID); + rpc UpdateNote(UpdateNoteIn) returns (Empty); + rpc GetNote(NoteID) returns (Note); + rpc RemoveNotes(RemoveNotesIn) returns (Empty); + rpc AddNoteTags(AddNoteTagsIn) returns (UInt32); + rpc UpdateNoteTags(UpdateNoteTagsIn) returns (UInt32); + rpc ClozeNumbersInNote(Note) returns (ClozeNumbersInNoteOut); + rpc AfterNoteUpdates(AfterNoteUpdatesIn) returns (Empty); + rpc FieldNamesForNotes(FieldNamesForNotesIn) returns (FieldNamesForNotesOut); + rpc NoteIsDuplicateOrEmpty(Note) returns (NoteIsDuplicateOrEmptyOut); + rpc CardsOfNote(NoteID) returns (CardIDs); +} + +service SyncService { + rpc SyncMedia(SyncAuth) returns (Empty); + rpc AbortSync(Empty) returns (Empty); + rpc AbortMediaSync(Empty) returns (Empty); + rpc BeforeUpload(Empty) returns (Empty); + rpc SyncLogin(SyncLoginIn) returns (SyncAuth); + rpc SyncStatus(SyncAuth) returns (SyncStatusOut); + rpc SyncCollection(SyncAuth) returns (SyncCollectionOut); + rpc FullUpload(SyncAuth) returns (Empty); + rpc FullDownload(SyncAuth) returns (Empty); + rpc SyncServerMethod(SyncServerMethodIn) returns (Json); +} + +service ConfigService { + rpc GetConfigJson(String) returns (Json); + rpc SetConfigJson(SetConfigJsonIn) returns (Empty); + rpc RemoveConfig(String) returns (Empty); + rpc GetAllConfig(Empty) returns (Json); + rpc GetConfigBool(Config.Bool) returns (Bool); + rpc SetConfigBool(SetConfigBoolIn) returns (Empty); + rpc GetConfigString(Config.String) returns (String); + rpc SetConfigString(SetConfigStringIn) returns (Empty); + rpc GetPreferences(Empty) returns (Preferences); + rpc SetPreferences(Preferences) returns (Empty); +} + +service NoteTypesService { + rpc AddOrUpdateNotetype(AddOrUpdateNotetypeIn) returns (NoteTypeID); + rpc GetStockNotetypeLegacy(StockNoteType) returns (Json); + rpc GetNotetypeLegacy(NoteTypeID) returns (Json); + rpc GetNotetypeNames(Empty) returns (NoteTypeNames); + rpc GetNotetypeNamesAndCounts(Empty) returns (NoteTypeUseCounts); + rpc GetNotetypeIDByName(String) returns (NoteTypeID); + rpc RemoveNotetype(NoteTypeID) returns (Empty); +} + service BackendService { rpc LatestProgress(Empty) returns (Progress); rpc SetWantsAbort(Empty) returns (Empty); @@ -145,19 +216,6 @@ service BackendService { rpc EmptyTrash(Empty) returns (Empty); rpc RestoreTrash(Empty) returns (Empty); - // decks - - rpc AddOrUpdateDeckLegacy(AddOrUpdateDeckLegacyIn) returns (DeckID); - rpc DeckTree(DeckTreeIn) returns (DeckTreeNode); - rpc DeckTreeLegacy(Empty) returns (Json); - rpc GetAllDecksLegacy(Empty) returns (Json); - rpc GetDeckIDByName(String) returns (DeckID); - rpc GetDeckLegacy(DeckID) returns (Json); - rpc GetDeckNames(GetDeckNamesIn) returns (DeckNames); - rpc NewDeckLegacy(Bool) returns (Json); - rpc RemoveDeck(DeckID) returns (Empty); - rpc DragDropDecks(DragDropDecksIn) returns (Empty); - // deck config rpc AddOrUpdateDeckConfigLegacy(AddOrUpdateDeckConfigLegacyIn) @@ -174,33 +232,6 @@ service BackendService { rpc RemoveCards(RemoveCardsIn) returns (Empty); rpc SetDeck(SetDeckIn) returns (Empty); - // notes - - rpc NewNote(NoteTypeID) returns (Note); - rpc AddNote(AddNoteIn) returns (NoteID); - rpc DefaultsForAdding(DefaultsForAddingIn) returns (DeckAndNotetype); - rpc DefaultDeckForNotetype(NoteTypeID) returns (DeckID); - rpc UpdateNote(UpdateNoteIn) returns (Empty); - rpc GetNote(NoteID) returns (Note); - rpc RemoveNotes(RemoveNotesIn) returns (Empty); - rpc AddNoteTags(AddNoteTagsIn) returns (UInt32); - rpc UpdateNoteTags(UpdateNoteTagsIn) returns (UInt32); - rpc ClozeNumbersInNote(Note) returns (ClozeNumbersInNoteOut); - rpc AfterNoteUpdates(AfterNoteUpdatesIn) returns (Empty); - rpc FieldNamesForNotes(FieldNamesForNotesIn) returns (FieldNamesForNotesOut); - rpc NoteIsDuplicateOrEmpty(Note) returns (NoteIsDuplicateOrEmptyOut); - rpc CardsOfNote(NoteID) returns (CardIDs); - - // note types - - rpc AddOrUpdateNotetype(AddOrUpdateNotetypeIn) returns (NoteTypeID); - rpc GetStockNotetypeLegacy(StockNoteType) returns (Json); - rpc GetNotetypeLegacy(NoteTypeID) returns (Json); - rpc GetNotetypeNames(Empty) returns (NoteTypeNames); - rpc GetNotetypeNamesAndCounts(Empty) returns (NoteTypeUseCounts); - rpc GetNotetypeIDByName(String) returns (NoteTypeID); - rpc RemoveNotetype(NoteTypeID) returns (Empty); - // collection rpc OpenCollection(OpenCollectionIn) returns (Empty); @@ -210,19 +241,6 @@ service BackendService { rpc Undo(Empty) returns (UndoStatus); rpc Redo(Empty) returns (UndoStatus); - // sync - - rpc SyncMedia(SyncAuth) returns (Empty); - rpc AbortSync(Empty) returns (Empty); - rpc AbortMediaSync(Empty) returns (Empty); - rpc BeforeUpload(Empty) returns (Empty); - rpc SyncLogin(SyncLoginIn) returns (SyncAuth); - rpc SyncStatus(SyncAuth) returns (SyncStatusOut); - rpc SyncCollection(SyncAuth) returns (SyncCollectionOut); - rpc FullUpload(SyncAuth) returns (Empty); - rpc FullDownload(SyncAuth) returns (Empty); - rpc SyncServerMethod(SyncServerMethodIn) returns (Json); - // translation/messages/text manipulation rpc TranslateString(TranslateStringIn) returns (String); @@ -238,22 +256,6 @@ service BackendService { rpc ClearTag(String) returns (Empty); rpc TagTree(Empty) returns (TagTreeNode); rpc DragDropTags(DragDropTagsIn) returns (Empty); - - // config - - rpc GetConfigJson(String) returns (Json); - rpc SetConfigJson(SetConfigJsonIn) returns (Empty); - rpc RemoveConfig(String) returns (Empty); - rpc GetAllConfig(Empty) returns (Json); - rpc GetConfigBool(Config.Bool) returns (Bool); - rpc SetConfigBool(SetConfigBoolIn) returns (Empty); - rpc GetConfigString(Config.String) returns (String); - rpc SetConfigString(SetConfigStringIn) returns (Empty); - - // preferences - - rpc GetPreferences(Empty) returns (Preferences); - rpc SetPreferences(Preferences) returns (Empty); } // Protobuf stored in .anki2 files diff --git a/rslib/build/protobuf.rs b/rslib/build/protobuf.rs index aad63055b..65c1c75ff 100644 --- a/rslib/build/protobuf.rs +++ b/rslib/build/protobuf.rs @@ -55,7 +55,7 @@ impl prost_build::ServiceGenerator for CustomGenerator { fn generate(&mut self, service: prost_build::Service, buf: &mut String) { write!( buf, - "pub mod {name} {{ + "pub mod {name}_service {{ use super::*; use prost::Message; use crate::err::Result; diff --git a/rslib/src/backend/config.rs b/rslib/src/backend/config.rs index 0deaa12e7..965be7e6d 100644 --- a/rslib/src/backend/config.rs +++ b/rslib/src/backend/config.rs @@ -1,12 +1,16 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +use super::Backend; use crate::{ backend_proto as pb, config::{BoolKey, StringKey}, + prelude::*, }; use pb::config::bool::Key as BoolKeyProto; use pb::config::string::Key as StringKeyProto; +pub(super) use pb::config_service::Service as ConfigService; +use serde_json::Value; impl From for BoolKey { fn from(k: BoolKeyProto) -> Self { @@ -38,3 +42,75 @@ impl From for StringKey { } } } + +impl ConfigService for Backend { + fn get_config_json(&self, input: pb::String) -> Result { + self.with_col(|col| { + let val: Option = col.get_config_optional(input.val.as_str()); + val.ok_or(AnkiError::NotFound) + .and_then(|v| serde_json::to_vec(&v).map_err(Into::into)) + .map(Into::into) + }) + } + + fn set_config_json(&self, input: pb::SetConfigJsonIn) -> Result { + self.with_col(|col| { + col.transact(None, |col| { + // ensure it's a well-formed object + let val: Value = serde_json::from_slice(&input.value_json)?; + col.set_config(input.key.as_str(), &val) + }) + }) + .map(Into::into) + } + + fn remove_config(&self, input: pb::String) -> Result { + self.with_col(|col| col.transact(None, |col| col.remove_config(input.val.as_str()))) + .map(Into::into) + } + + fn get_all_config(&self, _input: pb::Empty) -> Result { + self.with_col(|col| { + let conf = col.storage.get_all_config()?; + serde_json::to_vec(&conf).map_err(Into::into) + }) + .map(Into::into) + } + + fn get_config_bool(&self, input: pb::config::Bool) -> Result { + self.with_col(|col| { + Ok(pb::Bool { + val: col.get_bool(input.key().into()), + }) + }) + } + + fn set_config_bool(&self, input: pb::SetConfigBoolIn) -> Result { + self.with_col(|col| col.transact(None, |col| col.set_bool(input.key().into(), input.value))) + .map(Into::into) + } + + fn get_config_string(&self, input: pb::config::String) -> Result { + self.with_col(|col| { + Ok(pb::String { + val: col.get_string(input.key().into()), + }) + }) + } + + fn set_config_string(&self, input: pb::SetConfigStringIn) -> Result { + self.with_col(|col| { + col.transact(None, |col| col.set_string(input.key().into(), &input.value)) + }) + .map(Into::into) + } + + fn get_preferences(&self, _input: pb::Empty) -> Result { + self.with_col(|col| col.get_preferences()) + } + + fn set_preferences(&self, input: pb::Preferences) -> Result { + self.with_col(|col| col.set_preferences(input)) + .map(Into::into) + } +} diff --git a/rslib/src/backend/decks.rs b/rslib/src/backend/decks.rs new file mode 100644 index 000000000..e072f777f --- /dev/null +++ b/rslib/src/backend/decks.rs @@ -0,0 +1,127 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use super::Backend; +use crate::{ + backend_proto::{self as pb}, + decks::{Deck, DeckID, DeckSchema11}, + prelude::*, +}; +pub(super) use pb::decks_service::Service as DecksService; + +impl DecksService for Backend { + fn add_or_update_deck_legacy(&self, input: pb::AddOrUpdateDeckLegacyIn) -> Result { + self.with_col(|col| { + let schema11: DeckSchema11 = serde_json::from_slice(&input.deck)?; + let mut deck: Deck = schema11.into(); + if input.preserve_usn_and_mtime { + col.transact(None, |col| { + let usn = col.usn()?; + col.add_or_update_single_deck_with_existing_id(&mut deck, usn) + })?; + } else { + col.add_or_update_deck(&mut deck)?; + } + Ok(pb::DeckId { did: deck.id.0 }) + }) + } + + fn deck_tree(&self, input: pb::DeckTreeIn) -> Result { + let lim = if input.top_deck_id > 0 { + Some(DeckID(input.top_deck_id)) + } else { + None + }; + self.with_col(|col| { + let now = if input.now == 0 { + None + } else { + Some(TimestampSecs(input.now)) + }; + col.deck_tree(now, lim) + }) + } + + fn deck_tree_legacy(&self, _input: pb::Empty) -> Result { + self.with_col(|col| { + let tree = col.legacy_deck_tree()?; + serde_json::to_vec(&tree) + .map_err(Into::into) + .map(Into::into) + }) + } + + fn get_all_decks_legacy(&self, _input: pb::Empty) -> Result { + self.with_col(|col| { + let decks = col.storage.get_all_decks_as_schema11()?; + serde_json::to_vec(&decks).map_err(Into::into) + }) + .map(Into::into) + } + + fn get_deck_id_by_name(&self, input: pb::String) -> Result { + self.with_col(|col| { + col.get_deck_id(&input.val).and_then(|d| { + d.ok_or(AnkiError::NotFound) + .map(|d| pb::DeckId { did: d.0 }) + }) + }) + } + + fn get_deck_legacy(&self, input: pb::DeckId) -> Result { + self.with_col(|col| { + let deck: DeckSchema11 = col + .storage + .get_deck(input.into())? + .ok_or(AnkiError::NotFound)? + .into(); + serde_json::to_vec(&deck) + .map_err(Into::into) + .map(Into::into) + }) + } + + fn get_deck_names(&self, input: pb::GetDeckNamesIn) -> Result { + self.with_col(|col| { + let names = if input.include_filtered { + col.get_all_deck_names(input.skip_empty_default)? + } else { + col.get_all_normal_deck_names()? + }; + Ok(pb::DeckNames { + entries: names + .into_iter() + .map(|(id, name)| pb::DeckNameId { id: id.0, name }) + .collect(), + }) + }) + } + + fn new_deck_legacy(&self, input: pb::Bool) -> Result { + let deck = if input.val { + Deck::new_filtered() + } else { + Deck::new_normal() + }; + let schema11: DeckSchema11 = deck.into(); + serde_json::to_vec(&schema11) + .map_err(Into::into) + .map(Into::into) + } + + fn remove_deck(&self, input: pb::DeckId) -> Result { + self.with_col(|col| col.remove_deck_and_child_decks(input.into())) + .map(Into::into) + } + + fn drag_drop_decks(&self, input: pb::DragDropDecksIn) -> Result { + let source_dids: Vec<_> = input.source_deck_ids.into_iter().map(Into::into).collect(); + let target_did = if input.target_deck_id == 0 { + None + } else { + Some(input.target_deck_id.into()) + }; + self.with_col(|col| col.drag_drop_decks(&source_dids, target_did)) + .map(Into::into) + } +} diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index da51c86a5..ecc65e4f7 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -5,26 +5,33 @@ mod adding; mod card; mod config; mod dbproxy; +mod decks; mod err; mod generic; -mod http_sync_server; +mod notes; +mod notetypes; mod progress; mod scheduler; mod search; mod sync; -use self::scheduler::SchedulingService; -use crate::backend_proto::backend::Service as BackendService; +use self::{ + config::ConfigService, + decks::DecksService, + notes::NotesService, + notetypes::NoteTypesService, + scheduler::SchedulingService, + sync::{SyncService, SyncState}, +}; +use crate::backend_proto::backend_service::Service as BackendService; use crate::{ backend::dbproxy::db_command_bytes, backend_proto as pb, - backend_proto::{AddOrUpdateDeckConfigLegacyIn, Empty, RenderedTemplateReplacement}, + backend_proto::{AddOrUpdateDeckConfigLegacyIn, RenderedTemplateReplacement}, card::{Card, CardID}, - cloze::add_cloze_numbers_in_string, collection::{open_collection, Collection}, deckconf::{DeckConf, DeckConfSchema11}, - decks::{Deck, DeckID, DeckSchema11}, err::{AnkiError, Result}, i18n::I18n, latex::{extract_latex, extract_latex_expanding_clozes, ExtractedLatex}, @@ -33,26 +40,20 @@ use crate::{ markdown::render_markdown, media::check::MediaChecker, media::MediaManager, - notes::{Note, NoteID}, - notetype::{ - all_stock_notetypes, CardTemplateSchema11, NoteType, NoteTypeSchema11, RenderCardOutput, - }, + notes::NoteID, + notetype::{CardTemplateSchema11, RenderCardOutput}, scheduler::timespan::{answer_button_time, time_span}, search::{concatenate_searches, replace_search_node, write_nodes, Node}, - sync::{http::SyncRequest, LocalServer}, template::RenderedNode, text::{extract_av_tags, sanitize_html_no_images, strip_av_tags, AVTag}, - timestamp::TimestampSecs, undo::UndoableOpKind, }; use fluent::FluentValue; -use futures::future::AbortHandle; use log::error; use once_cell::sync::OnceCell; use progress::{AbortHandleSlot, Progress}; use prost::Message; -use serde_json::Value as JsonValue; -use std::{collections::HashSet, convert::TryInto}; +use std::convert::TryInto; use std::{ result, sync::{Arc, Mutex}, @@ -62,7 +63,6 @@ use tokio::runtime::{self, Runtime}; use self::{ err::anki_error_to_proto_error, progress::{progress_to_proto, ProgressState}, - sync::RemoteSyncStatus, }; pub struct Backend { @@ -79,9 +79,7 @@ pub struct Backend { #[derive(Default)] struct BackendState { - remote_sync_status: RemoteSyncStatus, - media_sync_abort: Option, - http_sync_server: Option, + sync: SyncState, } pub fn init_backend(init_msg: &[u8]) -> std::result::Result { @@ -100,12 +98,12 @@ pub fn init_backend(init_msg: &[u8]) -> std::result::Result { } impl BackendService for Backend { - fn latest_progress(&self, _input: Empty) -> Result { + fn latest_progress(&self, _input: pb::Empty) -> Result { let progress = self.progress_state.lock().unwrap().last_progress; Ok(progress_to_proto(progress, &self.i18n)) } - fn set_wants_abort(&self, _input: Empty) -> Result { + fn set_wants_abort(&self, _input: pb::Empty) -> Result { self.progress_state.lock().unwrap().want_abort = true; Ok(().into()) } @@ -304,7 +302,7 @@ impl BackendService for Backend { self.with_col(|col| col.get_graph_preferences()) } - fn set_graph_preferences(&self, input: pb::GraphPreferences) -> Result { + fn set_graph_preferences(&self, input: pb::GraphPreferences) -> Result { self.with_col(|col| col.set_graph_preferences(input)) .map(Into::into) } @@ -334,7 +332,7 @@ impl BackendService for Backend { }) } - fn trash_media_files(&self, input: pb::TrashMediaFilesIn) -> Result { + fn trash_media_files(&self, input: pb::TrashMediaFilesIn) -> Result { self.with_col(|col| { let mgr = MediaManager::new(&col.media_folder, &col.media_db)?; let mut ctx = mgr.dbctx(); @@ -354,7 +352,7 @@ impl BackendService for Backend { }) } - fn empty_trash(&self, _input: Empty) -> Result { + fn empty_trash(&self, _input: pb::Empty) -> Result { let mut handler = self.new_progress_handler(); let progress_fn = move |progress| handler.update(Progress::MediaCheck(progress as u32), true); @@ -370,7 +368,7 @@ impl BackendService for Backend { .map(Into::into) } - fn restore_trash(&self, _input: Empty) -> Result { + fn restore_trash(&self, _input: pb::Empty) -> Result { let mut handler = self.new_progress_handler(); let progress_fn = move |progress| handler.update(Progress::MediaCheck(progress as u32), true); @@ -386,124 +384,6 @@ impl BackendService for Backend { .map(Into::into) } - // decks - //---------------------------------------------------- - - fn add_or_update_deck_legacy(&self, input: pb::AddOrUpdateDeckLegacyIn) -> Result { - self.with_col(|col| { - let schema11: DeckSchema11 = serde_json::from_slice(&input.deck)?; - let mut deck: Deck = schema11.into(); - if input.preserve_usn_and_mtime { - col.transact(None, |col| { - let usn = col.usn()?; - col.add_or_update_single_deck_with_existing_id(&mut deck, usn) - })?; - } else { - col.add_or_update_deck(&mut deck)?; - } - Ok(pb::DeckId { did: deck.id.0 }) - }) - } - - fn deck_tree(&self, input: pb::DeckTreeIn) -> Result { - let lim = if input.top_deck_id > 0 { - Some(DeckID(input.top_deck_id)) - } else { - None - }; - self.with_col(|col| { - let now = if input.now == 0 { - None - } else { - Some(TimestampSecs(input.now)) - }; - col.deck_tree(now, lim) - }) - } - - fn deck_tree_legacy(&self, _input: pb::Empty) -> Result { - self.with_col(|col| { - let tree = col.legacy_deck_tree()?; - serde_json::to_vec(&tree) - .map_err(Into::into) - .map(Into::into) - }) - } - - fn get_all_decks_legacy(&self, _input: Empty) -> Result { - self.with_col(|col| { - let decks = col.storage.get_all_decks_as_schema11()?; - serde_json::to_vec(&decks).map_err(Into::into) - }) - .map(Into::into) - } - - fn get_deck_id_by_name(&self, input: pb::String) -> Result { - self.with_col(|col| { - col.get_deck_id(&input.val).and_then(|d| { - d.ok_or(AnkiError::NotFound) - .map(|d| pb::DeckId { did: d.0 }) - }) - }) - } - - fn get_deck_legacy(&self, input: pb::DeckId) -> Result { - self.with_col(|col| { - let deck: DeckSchema11 = col - .storage - .get_deck(input.into())? - .ok_or(AnkiError::NotFound)? - .into(); - serde_json::to_vec(&deck) - .map_err(Into::into) - .map(Into::into) - }) - } - - fn get_deck_names(&self, input: pb::GetDeckNamesIn) -> Result { - self.with_col(|col| { - let names = if input.include_filtered { - col.get_all_deck_names(input.skip_empty_default)? - } else { - col.get_all_normal_deck_names()? - }; - Ok(pb::DeckNames { - entries: names - .into_iter() - .map(|(id, name)| pb::DeckNameId { id: id.0, name }) - .collect(), - }) - }) - } - - fn new_deck_legacy(&self, input: pb::Bool) -> Result { - let deck = if input.val { - Deck::new_filtered() - } else { - Deck::new_normal() - }; - let schema11: DeckSchema11 = deck.into(); - serde_json::to_vec(&schema11) - .map_err(Into::into) - .map(Into::into) - } - - fn remove_deck(&self, input: pb::DeckId) -> Result { - self.with_col(|col| col.remove_deck_and_child_decks(input.into())) - .map(Into::into) - } - - fn drag_drop_decks(&self, input: pb::DragDropDecksIn) -> Result { - let source_dids: Vec<_> = input.source_deck_ids.into_iter().map(Into::into).collect(); - let target_did = if input.target_deck_id == 0 { - None - } else { - Some(input.target_deck_id.into()) - }; - self.with_col(|col| col.drag_drop_decks(&source_dids, target_did)) - .map(Into::into) - } - // deck config //---------------------------------------------------- @@ -522,7 +402,7 @@ impl BackendService for Backend { .map(Into::into) } - fn all_deck_config_legacy(&self, _input: Empty) -> Result { + fn all_deck_config_legacy(&self, _input: pb::Empty) -> Result { self.with_col(|col| { let conf: Vec = col .storage @@ -544,13 +424,13 @@ impl BackendService for Backend { .map(Into::into) } - fn new_deck_config_legacy(&self, _input: Empty) -> Result { + fn new_deck_config_legacy(&self, _input: pb::Empty) -> Result { serde_json::to_vec(&DeckConfSchema11::default()) .map_err(Into::into) .map(Into::into) } - fn remove_deck_config(&self, input: pb::DeckConfigId) -> Result { + fn remove_deck_config(&self, input: pb::DeckConfigId) -> Result { self.with_col(|col| col.transact(None, |col| col.remove_deck_config(input.into()))) .map(Into::into) } @@ -567,7 +447,7 @@ impl BackendService for Backend { }) } - fn update_card(&self, input: pb::UpdateCardIn) -> Result { + fn update_card(&self, input: pb::UpdateCardIn) -> Result { self.with_col(|col| { let op = if input.skip_undo_entry { None @@ -580,7 +460,7 @@ impl BackendService for Backend { .map(Into::into) } - fn remove_cards(&self, input: pb::RemoveCardsIn) -> Result { + fn remove_cards(&self, input: pb::RemoveCardsIn) -> Result { self.with_col(|col| { col.transact(None, |col| { col.remove_cards_and_orphaned_notes( @@ -595,250 +475,16 @@ impl BackendService for Backend { }) } - fn set_deck(&self, input: pb::SetDeckIn) -> Result { + fn set_deck(&self, input: pb::SetDeckIn) -> Result { let cids: Vec<_> = input.card_ids.into_iter().map(CardID).collect(); let deck_id = input.deck_id.into(); self.with_col(|col| col.set_deck(&cids, deck_id).map(Into::into)) } - // notes - //------------------------------------------------------------------- - - fn new_note(&self, input: pb::NoteTypeId) -> Result { - self.with_col(|col| { - let nt = col.get_notetype(input.into())?.ok_or(AnkiError::NotFound)?; - Ok(nt.new_note().into()) - }) - } - - fn add_note(&self, input: pb::AddNoteIn) -> Result { - self.with_col(|col| { - let mut note: Note = input.note.ok_or(AnkiError::NotFound)?.into(); - col.add_note(&mut note, DeckID(input.deck_id)) - .map(|_| pb::NoteId { nid: note.id.0 }) - }) - } - - fn defaults_for_adding(&self, input: pb::DefaultsForAddingIn) -> Result { - self.with_col(|col| { - let home_deck: DeckID = input.home_deck_of_current_review_card.into(); - col.defaults_for_adding(home_deck).map(Into::into) - }) - } - - fn default_deck_for_notetype(&self, input: pb::NoteTypeId) -> Result { - self.with_col(|col| { - Ok(col - .default_deck_for_notetype(input.into())? - .unwrap_or(DeckID(0)) - .into()) - }) - } - - fn update_note(&self, input: pb::UpdateNoteIn) -> Result { - self.with_col(|col| { - let op = if input.skip_undo_entry { - None - } else { - Some(UndoableOpKind::UpdateNote) - }; - let mut note: Note = input.note.ok_or(AnkiError::NotFound)?.into(); - col.update_note_with_op(&mut note, op) - }) - .map(Into::into) - } - - fn get_note(&self, input: pb::NoteId) -> Result { - self.with_col(|col| { - col.storage - .get_note(input.into())? - .ok_or(AnkiError::NotFound) - .map(Into::into) - }) - } - - fn remove_notes(&self, input: pb::RemoveNotesIn) -> Result { - self.with_col(|col| { - if !input.note_ids.is_empty() { - col.remove_notes( - &input - .note_ids - .into_iter() - .map(Into::into) - .collect::>(), - )?; - } - if !input.card_ids.is_empty() { - let nids = col.storage.note_ids_of_cards( - &input - .card_ids - .into_iter() - .map(Into::into) - .collect::>(), - )?; - col.remove_notes(&nids.into_iter().collect::>())? - } - Ok(().into()) - }) - } - - fn add_note_tags(&self, input: pb::AddNoteTagsIn) -> Result { - self.with_col(|col| { - col.add_tags_to_notes(&to_nids(input.nids), &input.tags) - .map(|n| n as u32) - }) - .map(Into::into) - } - - fn update_note_tags(&self, input: pb::UpdateNoteTagsIn) -> Result { - self.with_col(|col| { - col.replace_tags_for_notes( - &to_nids(input.nids), - &input.tags, - &input.replacement, - input.regex, - ) - .map(|n| (n as u32).into()) - }) - } - - fn cloze_numbers_in_note(&self, note: pb::Note) -> Result { - let mut set = HashSet::with_capacity(4); - for field in ¬e.fields { - add_cloze_numbers_in_string(field, &mut set); - } - Ok(pb::ClozeNumbersInNoteOut { - numbers: set.into_iter().map(|n| n as u32).collect(), - }) - } - - fn after_note_updates(&self, input: pb::AfterNoteUpdatesIn) -> Result { - self.with_col(|col| { - col.transact(None, |col| { - col.after_note_updates( - &to_nids(input.nids), - input.generate_cards, - input.mark_notes_modified, - )?; - Ok(pb::Empty {}) - }) - }) - } - - fn field_names_for_notes( - &self, - input: pb::FieldNamesForNotesIn, - ) -> Result { - self.with_col(|col| { - let nids: Vec<_> = input.nids.into_iter().map(NoteID).collect(); - col.storage - .field_names_for_notes(&nids) - .map(|fields| pb::FieldNamesForNotesOut { fields }) - }) - } - - fn note_is_duplicate_or_empty(&self, input: pb::Note) -> Result { - let note: Note = input.into(); - self.with_col(|col| { - col.note_is_duplicate_or_empty(¬e) - .map(|r| pb::NoteIsDuplicateOrEmptyOut { state: r as i32 }) - }) - } - - fn cards_of_note(&self, input: pb::NoteId) -> Result { - self.with_col(|col| { - col.storage - .all_card_ids_of_note(NoteID(input.nid)) - .map(|v| pb::CardIDs { - cids: v.into_iter().map(Into::into).collect(), - }) - }) - } - - // notetypes - //------------------------------------------------------------------- - - fn add_or_update_notetype(&self, input: pb::AddOrUpdateNotetypeIn) -> Result { - self.with_col(|col| { - let legacy: NoteTypeSchema11 = serde_json::from_slice(&input.json)?; - let mut nt: NoteType = legacy.into(); - if nt.id.0 == 0 { - col.add_notetype(&mut nt)?; - } else { - col.update_notetype(&mut nt, input.preserve_usn_and_mtime)?; - } - Ok(pb::NoteTypeId { ntid: nt.id.0 }) - }) - } - - fn get_stock_notetype_legacy(&self, input: pb::StockNoteType) -> Result { - // fixme: use individual functions instead of full vec - let mut all = all_stock_notetypes(&self.i18n); - let idx = (input.kind as usize).min(all.len() - 1); - let nt = all.swap_remove(idx); - let schema11: NoteTypeSchema11 = nt.into(); - serde_json::to_vec(&schema11) - .map_err(Into::into) - .map(Into::into) - } - - fn get_notetype_legacy(&self, input: pb::NoteTypeId) -> Result { - self.with_col(|col| { - let schema11: NoteTypeSchema11 = col - .storage - .get_notetype(input.into())? - .ok_or(AnkiError::NotFound)? - .into(); - Ok(serde_json::to_vec(&schema11)?).map(Into::into) - }) - } - - fn get_notetype_names(&self, _input: Empty) -> Result { - self.with_col(|col| { - let entries: Vec<_> = col - .storage - .get_all_notetype_names()? - .into_iter() - .map(|(id, name)| pb::NoteTypeNameId { id: id.0, name }) - .collect(); - Ok(pb::NoteTypeNames { entries }) - }) - } - - fn get_notetype_names_and_counts(&self, _input: Empty) -> Result { - self.with_col(|col| { - let entries: Vec<_> = col - .storage - .get_notetype_use_counts()? - .into_iter() - .map(|(id, name, use_count)| pb::NoteTypeNameIdUseCount { - id: id.0, - name, - use_count, - }) - .collect(); - Ok(pb::NoteTypeUseCounts { entries }) - }) - } - - fn get_notetype_id_by_name(&self, input: pb::String) -> Result { - self.with_col(|col| { - col.storage - .get_notetype_id(&input.val) - .and_then(|nt| nt.ok_or(AnkiError::NotFound)) - .map(|ntid| pb::NoteTypeId { ntid: ntid.0 }) - }) - } - - fn remove_notetype(&self, input: pb::NoteTypeId) -> Result { - self.with_col(|col| col.remove_notetype(input.into())) - .map(Into::into) - } - // collection //------------------------------------------------------------------- - fn open_collection(&self, input: pb::OpenCollectionIn) -> Result { + fn open_collection(&self, input: pb::OpenCollectionIn) -> Result { let mut col = self.col.lock().unwrap(); if col.is_some() { return Err(AnkiError::CollectionAlreadyOpen); @@ -867,7 +513,7 @@ impl BackendService for Backend { Ok(().into()) } - fn close_collection(&self, input: pb::CloseCollectionIn) -> Result { + fn close_collection(&self, input: pb::CloseCollectionIn) -> Result { self.abort_media_sync_and_wait(); let mut col = self.col.lock().unwrap(); @@ -917,60 +563,6 @@ impl BackendService for Backend { }) } - // sync - //------------------------------------------------------------------- - - fn sync_media(&self, input: pb::SyncAuth) -> Result { - self.sync_media_inner(input).map(Into::into) - } - - fn abort_sync(&self, _input: Empty) -> Result { - if let Some(handle) = self.sync_abort.lock().unwrap().take() { - handle.abort(); - } - Ok(().into()) - } - - /// Abort the media sync. Does not wait for completion. - fn abort_media_sync(&self, _input: Empty) -> Result { - let guard = self.state.lock().unwrap(); - if let Some(handle) = &guard.media_sync_abort { - handle.abort(); - } - Ok(().into()) - } - - fn before_upload(&self, _input: Empty) -> Result { - self.with_col(|col| col.before_upload().map(Into::into)) - } - - fn sync_login(&self, input: pb::SyncLoginIn) -> Result { - self.sync_login_inner(input) - } - - fn sync_status(&self, input: pb::SyncAuth) -> Result { - self.sync_status_inner(input) - } - - fn sync_collection(&self, input: pb::SyncAuth) -> Result { - self.sync_collection_inner(input) - } - - fn full_upload(&self, input: pb::SyncAuth) -> Result { - self.full_sync_inner(input, true)?; - Ok(().into()) - } - - fn full_download(&self, input: pb::SyncAuth) -> Result { - self.full_sync_inner(input, false)?; - Ok(().into()) - } - - fn sync_server_method(&self, input: pb::SyncServerMethodIn) -> Result { - let req = SyncRequest::from_method_and_data(input.method(), input.data)?; - self.sync_server_method_inner(req).map(Into::into) - } - // i18n/messages //------------------------------------------------------------------- @@ -999,7 +591,7 @@ impl BackendService for Backend { .into()) } - fn i18n_resources(&self, _input: Empty) -> Result { + fn i18n_resources(&self, _input: pb::Empty) -> Result { serde_json::to_vec(&self.i18n.resources_for_js()) .map(Into::into) .map_err(Into::into) @@ -1021,7 +613,7 @@ impl BackendService for Backend { self.with_col(|col| col.transact(None, |col| col.clear_unused_tags().map(Into::into))) } - fn all_tags(&self, _input: Empty) -> Result { + fn all_tags(&self, _input: pb::Empty) -> Result { Ok(pb::StringList { vals: self.with_col(|col| { Ok(col @@ -1052,11 +644,11 @@ impl BackendService for Backend { }) } - fn tag_tree(&self, _input: Empty) -> Result { + fn tag_tree(&self, _input: pb::Empty) -> Result { self.with_col(|col| col.tag_tree()) } - fn drag_drop_tags(&self, input: pb::DragDropTagsIn) -> Result { + fn drag_drop_tags(&self, input: pb::DragDropTagsIn) -> Result { let source_tags = input.source_tags; let target_tag = if input.target_tag.is_empty() { None @@ -1066,79 +658,6 @@ impl BackendService for Backend { self.with_col(|col| col.drag_drop_tags(&source_tags, target_tag)) .map(Into::into) } - - // config/preferences - //------------------------------------------------------------------- - - fn get_config_json(&self, input: pb::String) -> Result { - self.with_col(|col| { - let val: Option = col.get_config_optional(input.val.as_str()); - val.ok_or(AnkiError::NotFound) - .and_then(|v| serde_json::to_vec(&v).map_err(Into::into)) - .map(Into::into) - }) - } - - fn set_config_json(&self, input: pb::SetConfigJsonIn) -> Result { - self.with_col(|col| { - col.transact(None, |col| { - // ensure it's a well-formed object - let val: JsonValue = serde_json::from_slice(&input.value_json)?; - col.set_config(input.key.as_str(), &val) - }) - }) - .map(Into::into) - } - - fn remove_config(&self, input: pb::String) -> Result { - self.with_col(|col| col.transact(None, |col| col.remove_config(input.val.as_str()))) - .map(Into::into) - } - - fn get_all_config(&self, _input: Empty) -> Result { - self.with_col(|col| { - let conf = col.storage.get_all_config()?; - serde_json::to_vec(&conf).map_err(Into::into) - }) - .map(Into::into) - } - - fn get_config_bool(&self, input: pb::config::Bool) -> Result { - self.with_col(|col| { - Ok(pb::Bool { - val: col.get_bool(input.key().into()), - }) - }) - } - - fn set_config_bool(&self, input: pb::SetConfigBoolIn) -> Result { - self.with_col(|col| col.transact(None, |col| col.set_bool(input.key().into(), input.value))) - .map(Into::into) - } - - fn get_config_string(&self, input: pb::config::String) -> Result { - self.with_col(|col| { - Ok(pb::String { - val: col.get_string(input.key().into()), - }) - }) - } - - fn set_config_string(&self, input: pb::SetConfigStringIn) -> Result { - self.with_col(|col| { - col.transact(None, |col| col.set_string(input.key().into(), &input.value)) - }) - .map(Into::into) - } - - fn get_preferences(&self, _input: Empty) -> Result { - self.with_col(|col| col.get_preferences()) - } - - fn set_preferences(&self, input: pb::Preferences) -> Result { - self.with_col(|col| col.set_preferences(input)) - .map(Into::into) - } } impl Backend { @@ -1172,6 +691,11 @@ impl Backend { .and_then(|service| match service { pb::ServiceIndex::Scheduling => SchedulingService::run_method(self, method, input), pb::ServiceIndex::Backend => BackendService::run_method(self, method, input), + pb::ServiceIndex::Decks => DecksService::run_method(self, method, input), + pb::ServiceIndex::Notes => NotesService::run_method(self, method, input), + pb::ServiceIndex::Notetypes => NoteTypesService::run_method(self, method, input), + pb::ServiceIndex::Config => ConfigService::run_method(self, method, input), + pb::ServiceIndex::Sync => SyncService::run_method(self, method, input), }) .map_err(|err| { let backend_err = anki_error_to_proto_error(err, &self.i18n); @@ -1225,10 +749,6 @@ impl Backend { } } -fn to_nids(ids: Vec) -> Vec { - ids.into_iter().map(NoteID).collect() -} - fn translate_arg_to_fluent_val(arg: &pb::TranslateArgValue) -> FluentValue { use pb::translate_arg_value::Value as V; match &arg.value { diff --git a/rslib/src/backend/notes.rs b/rslib/src/backend/notes.rs new file mode 100644 index 000000000..5c82ad5a4 --- /dev/null +++ b/rslib/src/backend/notes.rs @@ -0,0 +1,172 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use std::collections::HashSet; + +use super::Backend; +use crate::{ + backend_proto::{self as pb}, + cloze::add_cloze_numbers_in_string, + prelude::*, +}; +pub(super) use pb::notes_service::Service as NotesService; + +impl NotesService for Backend { + // notes + //------------------------------------------------------------------- + + fn new_note(&self, input: pb::NoteTypeId) -> Result { + self.with_col(|col| { + let nt = col.get_notetype(input.into())?.ok_or(AnkiError::NotFound)?; + Ok(nt.new_note().into()) + }) + } + + fn add_note(&self, input: pb::AddNoteIn) -> Result { + self.with_col(|col| { + let mut note: Note = input.note.ok_or(AnkiError::NotFound)?.into(); + col.add_note(&mut note, DeckID(input.deck_id)) + .map(|_| pb::NoteId { nid: note.id.0 }) + }) + } + + fn defaults_for_adding(&self, input: pb::DefaultsForAddingIn) -> Result { + self.with_col(|col| { + let home_deck: DeckID = input.home_deck_of_current_review_card.into(); + col.defaults_for_adding(home_deck).map(Into::into) + }) + } + + fn default_deck_for_notetype(&self, input: pb::NoteTypeId) -> Result { + self.with_col(|col| { + Ok(col + .default_deck_for_notetype(input.into())? + .unwrap_or(DeckID(0)) + .into()) + }) + } + + fn update_note(&self, input: pb::UpdateNoteIn) -> Result { + self.with_col(|col| { + let op = if input.skip_undo_entry { + None + } else { + Some(UndoableOpKind::UpdateNote) + }; + let mut note: Note = input.note.ok_or(AnkiError::NotFound)?.into(); + col.update_note_with_op(&mut note, op) + }) + .map(Into::into) + } + + fn get_note(&self, input: pb::NoteId) -> Result { + self.with_col(|col| { + col.storage + .get_note(input.into())? + .ok_or(AnkiError::NotFound) + .map(Into::into) + }) + } + + fn remove_notes(&self, input: pb::RemoveNotesIn) -> Result { + self.with_col(|col| { + if !input.note_ids.is_empty() { + col.remove_notes( + &input + .note_ids + .into_iter() + .map(Into::into) + .collect::>(), + )?; + } + if !input.card_ids.is_empty() { + let nids = col.storage.note_ids_of_cards( + &input + .card_ids + .into_iter() + .map(Into::into) + .collect::>(), + )?; + col.remove_notes(&nids.into_iter().collect::>())? + } + Ok(().into()) + }) + } + + fn add_note_tags(&self, input: pb::AddNoteTagsIn) -> Result { + self.with_col(|col| { + col.add_tags_to_notes(&to_nids(input.nids), &input.tags) + .map(|n| n as u32) + }) + .map(Into::into) + } + + fn update_note_tags(&self, input: pb::UpdateNoteTagsIn) -> Result { + self.with_col(|col| { + col.replace_tags_for_notes( + &to_nids(input.nids), + &input.tags, + &input.replacement, + input.regex, + ) + .map(|n| (n as u32).into()) + }) + } + + fn cloze_numbers_in_note(&self, note: pb::Note) -> Result { + let mut set = HashSet::with_capacity(4); + for field in ¬e.fields { + add_cloze_numbers_in_string(field, &mut set); + } + Ok(pb::ClozeNumbersInNoteOut { + numbers: set.into_iter().map(|n| n as u32).collect(), + }) + } + + fn after_note_updates(&self, input: pb::AfterNoteUpdatesIn) -> Result { + self.with_col(|col| { + col.transact(None, |col| { + col.after_note_updates( + &to_nids(input.nids), + input.generate_cards, + input.mark_notes_modified, + )?; + Ok(pb::Empty {}) + }) + }) + } + + fn field_names_for_notes( + &self, + input: pb::FieldNamesForNotesIn, + ) -> Result { + self.with_col(|col| { + let nids: Vec<_> = input.nids.into_iter().map(NoteID).collect(); + col.storage + .field_names_for_notes(&nids) + .map(|fields| pb::FieldNamesForNotesOut { fields }) + }) + } + + fn note_is_duplicate_or_empty(&self, input: pb::Note) -> Result { + let note: Note = input.into(); + self.with_col(|col| { + col.note_is_duplicate_or_empty(¬e) + .map(|r| pb::NoteIsDuplicateOrEmptyOut { state: r as i32 }) + }) + } + + fn cards_of_note(&self, input: pb::NoteId) -> Result { + self.with_col(|col| { + col.storage + .all_card_ids_of_note(NoteID(input.nid)) + .map(|v| pb::CardIDs { + cids: v.into_iter().map(Into::into).collect(), + }) + }) + } +} + +fn to_nids(ids: Vec) -> Vec { + ids.into_iter().map(NoteID).collect() +} diff --git a/rslib/src/backend/notetypes.rs b/rslib/src/backend/notetypes.rs new file mode 100644 index 000000000..cab29ba03 --- /dev/null +++ b/rslib/src/backend/notetypes.rs @@ -0,0 +1,89 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use super::Backend; +use crate::{ + backend_proto as pb, + notetype::{all_stock_notetypes, NoteType, NoteTypeSchema11}, + prelude::*, +}; +pub(super) use pb::notetypes_service::Service as NoteTypesService; + +impl NoteTypesService for Backend { + fn add_or_update_notetype(&self, input: pb::AddOrUpdateNotetypeIn) -> Result { + self.with_col(|col| { + let legacy: NoteTypeSchema11 = serde_json::from_slice(&input.json)?; + let mut nt: NoteType = legacy.into(); + if nt.id.0 == 0 { + col.add_notetype(&mut nt)?; + } else { + col.update_notetype(&mut nt, input.preserve_usn_and_mtime)?; + } + Ok(pb::NoteTypeId { ntid: nt.id.0 }) + }) + } + + fn get_stock_notetype_legacy(&self, input: pb::StockNoteType) -> Result { + // fixme: use individual functions instead of full vec + let mut all = all_stock_notetypes(&self.i18n); + let idx = (input.kind as usize).min(all.len() - 1); + let nt = all.swap_remove(idx); + let schema11: NoteTypeSchema11 = nt.into(); + serde_json::to_vec(&schema11) + .map_err(Into::into) + .map(Into::into) + } + + fn get_notetype_legacy(&self, input: pb::NoteTypeId) -> Result { + self.with_col(|col| { + let schema11: NoteTypeSchema11 = col + .storage + .get_notetype(input.into())? + .ok_or(AnkiError::NotFound)? + .into(); + Ok(serde_json::to_vec(&schema11)?).map(Into::into) + }) + } + + fn get_notetype_names(&self, _input: pb::Empty) -> Result { + self.with_col(|col| { + let entries: Vec<_> = col + .storage + .get_all_notetype_names()? + .into_iter() + .map(|(id, name)| pb::NoteTypeNameId { id: id.0, name }) + .collect(); + Ok(pb::NoteTypeNames { entries }) + }) + } + + fn get_notetype_names_and_counts(&self, _input: pb::Empty) -> Result { + self.with_col(|col| { + let entries: Vec<_> = col + .storage + .get_notetype_use_counts()? + .into_iter() + .map(|(id, name, use_count)| pb::NoteTypeNameIdUseCount { + id: id.0, + name, + use_count, + }) + .collect(); + Ok(pb::NoteTypeUseCounts { entries }) + }) + } + + fn get_notetype_id_by_name(&self, input: pb::String) -> Result { + self.with_col(|col| { + col.storage + .get_notetype_id(&input.val) + .and_then(|nt| nt.ok_or(AnkiError::NotFound)) + .map(|ntid| pb::NoteTypeId { ntid: ntid.0 }) + }) + } + + fn remove_notetype(&self, input: pb::NoteTypeId) -> Result { + self.with_col(|col| col.remove_notetype(input.into())) + .map(Into::into) + } +} diff --git a/rslib/src/backend/scheduler/mod.rs b/rslib/src/backend/scheduler/mod.rs index 73b0b1536..1e8a0a19c 100644 --- a/rslib/src/backend/scheduler/mod.rs +++ b/rslib/src/backend/scheduler/mod.rs @@ -15,7 +15,7 @@ use crate::{ }, stats::studied_today, }; -pub(super) use pb::scheduling::Service as SchedulingService; +pub(super) use pb::scheduling_service::Service as SchedulingService; impl SchedulingService for Backend { /// This behaves like _updateCutoff() in older code - it also unburies at the start of diff --git a/rslib/src/backend/sync.rs b/rslib/src/backend/sync/mod.rs similarity index 78% rename from rslib/src/backend/sync.rs rename to rslib/src/backend/sync/mod.rs index 17c1fd5b0..7d894ee0a 100644 --- a/rslib/src/backend/sync.rs +++ b/rslib/src/backend/sync/mod.rs @@ -1,6 +1,8 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +mod server; + use std::sync::Arc; use futures::future::{AbortHandle, AbortRegistration, Abortable}; @@ -12,12 +14,20 @@ use crate::{ media::MediaManager, prelude::*, sync::{ - get_remote_sync_meta, sync_abort, sync_login, FullSyncProgress, NormalSyncProgress, - SyncActionRequired, SyncAuth, SyncMeta, SyncOutput, + get_remote_sync_meta, http::SyncRequest, sync_abort, sync_login, FullSyncProgress, + LocalServer, NormalSyncProgress, SyncActionRequired, SyncAuth, SyncMeta, SyncOutput, }, }; use super::{progress::AbortHandleSlot, Backend}; +pub(super) use pb::sync_service::Service as SyncService; + +#[derive(Default)] +pub(super) struct SyncState { + remote_sync_status: RemoteSyncStatus, + media_sync_abort: Option, + http_sync_server: Option, +} #[derive(Default, Debug)] pub(super) struct RemoteSyncStatus { @@ -70,6 +80,59 @@ impl From for SyncAuth { } } +impl SyncService for Backend { + fn sync_media(&self, input: pb::SyncAuth) -> Result { + self.sync_media_inner(input).map(Into::into) + } + + fn abort_sync(&self, _input: pb::Empty) -> Result { + if let Some(handle) = self.sync_abort.lock().unwrap().take() { + handle.abort(); + } + Ok(().into()) + } + + /// Abort the media sync. Does not wait for completion. + fn abort_media_sync(&self, _input: pb::Empty) -> Result { + let guard = self.state.lock().unwrap(); + if let Some(handle) = &guard.sync.media_sync_abort { + handle.abort(); + } + Ok(().into()) + } + + fn before_upload(&self, _input: pb::Empty) -> Result { + self.with_col(|col| col.before_upload().map(Into::into)) + } + + fn sync_login(&self, input: pb::SyncLoginIn) -> Result { + self.sync_login_inner(input) + } + + fn sync_status(&self, input: pb::SyncAuth) -> Result { + self.sync_status_inner(input) + } + + fn sync_collection(&self, input: pb::SyncAuth) -> Result { + self.sync_collection_inner(input) + } + + fn full_upload(&self, input: pb::SyncAuth) -> Result { + self.full_sync_inner(input, true)?; + Ok(().into()) + } + + fn full_download(&self, input: pb::SyncAuth) -> Result { + self.full_sync_inner(input, false)?; + Ok(().into()) + } + + fn sync_server_method(&self, input: pb::SyncServerMethodIn) -> Result { + let req = SyncRequest::from_method_and_data(input.method(), input.data)?; + self.sync_server_method_inner(req).map(Into::into) + } +} + impl Backend { fn sync_abort_handle( &self, @@ -104,11 +167,11 @@ impl Backend { let (abort_handle, abort_reg) = AbortHandle::new_pair(); { let mut guard = self.state.lock().unwrap(); - if guard.media_sync_abort.is_some() { + if guard.sync.media_sync_abort.is_some() { // media sync is already active return Ok(()); } else { - guard.media_sync_abort = Some(abort_handle); + guard.sync.media_sync_abort = Some(abort_handle); } } @@ -131,7 +194,7 @@ impl Backend { let result = rt.block_on(abortable_sync); // mark inactive - self.state.lock().unwrap().media_sync_abort.take(); + self.state.lock().unwrap().sync.media_sync_abort.take(); // return result match result { @@ -146,14 +209,14 @@ impl Backend { /// Abort the media sync. Won't return until aborted. pub(super) fn abort_media_sync_and_wait(&self) { let guard = self.state.lock().unwrap(); - if let Some(handle) = &guard.media_sync_abort { + if let Some(handle) = &guard.sync.media_sync_abort { handle.abort(); self.progress_state.lock().unwrap().want_abort = true; } drop(guard); // block until it aborts - while self.state.lock().unwrap().media_sync_abort.is_some() { + while self.state.lock().unwrap().sync.media_sync_abort.is_some() { std::thread::sleep(std::time::Duration::from_millis(100)); self.progress_state.lock().unwrap().want_abort = true; } @@ -185,8 +248,8 @@ impl Backend { // return cached server response if only a short time has elapsed { let guard = self.state.lock().unwrap(); - if guard.remote_sync_status.last_check.elapsed_secs() < 300 { - return Ok(guard.remote_sync_status.last_response.into()); + if guard.sync.remote_sync_status.last_check.elapsed_secs() < 300 { + return Ok(guard.sync.remote_sync_status.last_response.into()); } } @@ -201,9 +264,9 @@ impl Backend { // On startup, the sync status check will block on network access, and then automatic syncing begins, // taking hold of the mutex. By the time we reach here, our network status may be out of date, // so we discard it if stale. - if guard.remote_sync_status.last_check < time_at_check_begin { - guard.remote_sync_status.last_check = time_at_check_begin; - guard.remote_sync_status.last_response = response; + if guard.sync.remote_sync_status.last_check < time_at_check_begin { + guard.sync.remote_sync_status.last_check = time_at_check_begin; + guard.sync.remote_sync_status.last_response = response; } } @@ -247,6 +310,7 @@ impl Backend { self.state .lock() .unwrap() + .sync .remote_sync_status .update(output.required.into()); Ok(output.into()) @@ -302,6 +366,7 @@ impl Backend { self.state .lock() .unwrap() + .sync .remote_sync_status .update(pb::sync_status_out::Required::NoChanges); } diff --git a/rslib/src/backend/http_sync_server.rs b/rslib/src/backend/sync/server.rs similarity index 94% rename from rslib/src/backend/http_sync_server.rs rename to rslib/src/backend/sync/server.rs index efdc9f3ab..8b4816e9f 100644 --- a/rslib/src/backend/http_sync_server.rs +++ b/rslib/src/backend/sync/server.rs @@ -4,7 +4,7 @@ use std::{path::PathBuf, sync::MutexGuard}; use tokio::runtime::Runtime; -use super::{Backend, BackendState}; +use crate::backend::{Backend, BackendState}; use crate::{ err::SyncErrorKind, prelude::*, @@ -24,16 +24,12 @@ impl Backend { F: FnOnce(&mut LocalServer) -> Result, { let mut state_guard = self.state.lock().unwrap(); - let out = - func( - state_guard - .http_sync_server - .as_mut() - .ok_or_else(|| AnkiError::SyncError { - kind: SyncErrorKind::SyncNotStarted, - info: Default::default(), - })?, - ); + let out = func(state_guard.sync.http_sync_server.as_mut().ok_or_else(|| { + AnkiError::SyncError { + kind: SyncErrorKind::SyncNotStarted, + info: Default::default(), + } + })?); if out.is_err() { self.abort_and_restore_collection(Some(state_guard)) } @@ -82,6 +78,7 @@ impl Backend { fn take_server(&self, state_guard: Option>) -> Result { let mut state_guard = state_guard.unwrap_or_else(|| self.state.lock().unwrap()); state_guard + .sync .http_sync_server .take() .ok_or_else(|| AnkiError::SyncError { @@ -94,7 +91,7 @@ impl Backend { // place col into new server let server = self.col_into_server()?; let mut state_guard = self.state.lock().unwrap(); - assert!(state_guard.http_sync_server.replace(server).is_none()); + assert!(state_guard.sync.http_sync_server.replace(server).is_none()); drop(state_guard); self.with_sync_server(|server| { From cd14987812422a26dcd8c0d12fe0dc788a4bf858 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 11 Mar 2021 16:05:06 +1000 Subject: [PATCH 09/13] split out tags, deck config and card rendering --- pylib/anki/_backend/genbackend.py | 4 +- rslib/backend.proto | 53 +++---- rslib/src/backend/cardrendering.rs | 119 +++++++++++++++ rslib/src/backend/deckconfig.rs | 60 ++++++++ rslib/src/backend/mod.rs | 232 ++--------------------------- rslib/src/backend/tags.rs | 58 ++++++++ 6 files changed, 283 insertions(+), 243 deletions(-) create mode 100644 rslib/src/backend/cardrendering.rs create mode 100644 rslib/src/backend/deckconfig.rs create mode 100644 rslib/src/backend/tags.rs diff --git a/pylib/anki/_backend/genbackend.py b/pylib/anki/_backend/genbackend.py index 31c44ee86..633fe1f88 100755 --- a/pylib/anki/_backend/genbackend.py +++ b/pylib/anki/_backend/genbackend.py @@ -160,7 +160,9 @@ def render_service( for service in pb.ServiceIndex.DESCRIPTOR.values: # SERVICE_INDEX_TEST -> _TESTSERVICE - service_var = service.name.replace("SERVICE_INDEX", "") + "SERVICE" + service_var = ( + "_" + service.name.replace("SERVICE_INDEX", "").replace("_", "") + "SERVICE" + ) service_obj = getattr(pb, service_var) service_index = service.number render_service(service_obj, service_index) diff --git a/rslib/backend.proto b/rslib/backend.proto index a7b02f105..9ed4e824d 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -83,8 +83,11 @@ enum ServiceIndex { SERVICE_INDEX_DECKS = 1; SERVICE_INDEX_NOTES = 2; SERVICE_INDEX_SYNC = 3; - SERVICE_INDEX_NOTETYPES = 4; + SERVICE_INDEX_NOTE_TYPES = 4; SERVICE_INDEX_CONFIG = 5; + SERVICE_INDEX_CARD_RENDERING = 6; + SERVICE_INDEX_DECK_CONFIG = 7; + SERVICE_INDEX_TAGS = 8; SERVICE_INDEX_BACKEND = 99; } @@ -179,18 +182,36 @@ service NoteTypesService { rpc RemoveNotetype(NoteTypeID) returns (Empty); } -service BackendService { - rpc LatestProgress(Empty) returns (Progress); - rpc SetWantsAbort(Empty) returns (Empty); - - // card rendering - +service CardRenderingService { rpc ExtractAVTags(ExtractAVTagsIn) returns (ExtractAVTagsOut); rpc ExtractLatex(ExtractLatexIn) returns (ExtractLatexOut); rpc GetEmptyCards(Empty) returns (EmptyCardsReport); rpc RenderExistingCard(RenderExistingCardIn) returns (RenderCardOut); rpc RenderUncommittedCard(RenderUncommittedCardIn) returns (RenderCardOut); rpc StripAVTags(String) returns (String); +} + +service DeckConfigService { + rpc AddOrUpdateDeckConfigLegacy(AddOrUpdateDeckConfigLegacyIn) + returns (DeckConfigID); + rpc AllDeckConfigLegacy(Empty) returns (Json); + rpc GetDeckConfigLegacy(DeckConfigID) returns (Json); + rpc NewDeckConfigLegacy(Empty) returns (Json); + rpc RemoveDeckConfig(DeckConfigID) returns (Empty); +} + +service TagsService { + rpc ClearUnusedTags(Empty) returns (Empty); + rpc AllTags(Empty) returns (StringList); + rpc SetTagExpanded(SetTagExpandedIn) returns (Empty); + rpc ClearTag(String) returns (Empty); + rpc TagTree(Empty) returns (TagTreeNode); + rpc DragDropTags(DragDropTagsIn) returns (Empty); +} + +service BackendService { + rpc LatestProgress(Empty) returns (Progress); + rpc SetWantsAbort(Empty) returns (Empty); // searching @@ -216,15 +237,6 @@ service BackendService { rpc EmptyTrash(Empty) returns (Empty); rpc RestoreTrash(Empty) returns (Empty); - // deck config - - rpc AddOrUpdateDeckConfigLegacy(AddOrUpdateDeckConfigLegacyIn) - returns (DeckConfigID); - rpc AllDeckConfigLegacy(Empty) returns (Json); - rpc GetDeckConfigLegacy(DeckConfigID) returns (Json); - rpc NewDeckConfigLegacy(Empty) returns (Json); - rpc RemoveDeckConfig(DeckConfigID) returns (Empty); - // cards rpc GetCard(CardID) returns (Card); @@ -247,15 +259,6 @@ service BackendService { rpc FormatTimespan(FormatTimespanIn) returns (String); rpc I18nResources(Empty) returns (Json); rpc RenderMarkdown(RenderMarkdownIn) returns (String); - - // tags - - rpc ClearUnusedTags(Empty) returns (Empty); - rpc AllTags(Empty) returns (StringList); - rpc SetTagExpanded(SetTagExpandedIn) returns (Empty); - rpc ClearTag(String) returns (Empty); - rpc TagTree(Empty) returns (TagTreeNode); - rpc DragDropTags(DragDropTagsIn) returns (Empty); } // Protobuf stored in .anki2 files diff --git a/rslib/src/backend/cardrendering.rs b/rslib/src/backend/cardrendering.rs new file mode 100644 index 000000000..9bbe8b805 --- /dev/null +++ b/rslib/src/backend/cardrendering.rs @@ -0,0 +1,119 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use super::Backend; +use crate::{ + backend_proto as pb, + latex::{extract_latex, extract_latex_expanding_clozes, ExtractedLatex}, + notetype::CardTemplateSchema11, + prelude::*, + text::{extract_av_tags, strip_av_tags, AVTag}, +}; +pub(super) use pb::cardrendering_service::Service as CardRenderingService; + +impl CardRenderingService for Backend { + fn extract_av_tags(&self, input: pb::ExtractAvTagsIn) -> Result { + let (text, tags) = extract_av_tags(&input.text, input.question_side); + let pt_tags = tags + .into_iter() + .map(|avtag| match avtag { + AVTag::SoundOrVideo(file) => pb::AvTag { + value: Some(pb::av_tag::Value::SoundOrVideo(file)), + }, + AVTag::TextToSpeech { + field_text, + lang, + voices, + other_args, + speed, + } => pb::AvTag { + value: Some(pb::av_tag::Value::Tts(pb::TtsTag { + field_text, + lang, + voices, + other_args, + speed, + })), + }, + }) + .collect(); + + Ok(pb::ExtractAvTagsOut { + text: text.into(), + av_tags: pt_tags, + }) + } + + fn extract_latex(&self, input: pb::ExtractLatexIn) -> Result { + let func = if input.expand_clozes { + extract_latex_expanding_clozes + } else { + extract_latex + }; + let (text, extracted) = func(&input.text, input.svg); + + Ok(pb::ExtractLatexOut { + text, + latex: extracted + .into_iter() + .map(|e: ExtractedLatex| pb::ExtractedLatex { + filename: e.fname, + latex_body: e.latex, + }) + .collect(), + }) + } + + fn get_empty_cards(&self, _input: pb::Empty) -> Result { + self.with_col(|col| { + let mut empty = col.empty_cards()?; + let report = col.empty_cards_report(&mut empty)?; + + let mut outnotes = vec![]; + for (_ntid, notes) in empty { + outnotes.extend(notes.into_iter().map(|e| { + pb::empty_cards_report::NoteWithEmptyCards { + note_id: e.nid.0, + will_delete_note: e.empty.len() == e.current_count, + card_ids: e.empty.into_iter().map(|(_ord, id)| id.0).collect(), + } + })) + } + Ok(pb::EmptyCardsReport { + report, + notes: outnotes, + }) + }) + } + + fn render_existing_card(&self, input: pb::RenderExistingCardIn) -> Result { + self.with_col(|col| { + col.render_existing_card(CardID(input.card_id), input.browser) + .map(Into::into) + }) + } + + fn render_uncommitted_card( + &self, + input: pb::RenderUncommittedCardIn, + ) -> Result { + let schema11: CardTemplateSchema11 = serde_json::from_slice(&input.template)?; + let template = schema11.into(); + let mut note = input + .note + .ok_or_else(|| AnkiError::invalid_input("missing note"))? + .into(); + let ord = input.card_ord as u16; + let fill_empty = input.fill_empty; + self.with_col(|col| { + col.render_uncommitted_card(&mut note, &template, ord, fill_empty) + .map(Into::into) + }) + } + + fn strip_av_tags(&self, input: pb::String) -> Result { + Ok(pb::String { + val: strip_av_tags(&input.val).into(), + }) + } +} diff --git a/rslib/src/backend/deckconfig.rs b/rslib/src/backend/deckconfig.rs new file mode 100644 index 000000000..9b20fe8f7 --- /dev/null +++ b/rslib/src/backend/deckconfig.rs @@ -0,0 +1,60 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use super::Backend; +use crate::{ + backend_proto as pb, + deckconf::{DeckConf, DeckConfSchema11}, + prelude::*, +}; +pub(super) use pb::deckconfig_service::Service as DeckConfigService; + +impl DeckConfigService for Backend { + fn add_or_update_deck_config_legacy( + &self, + input: pb::AddOrUpdateDeckConfigLegacyIn, + ) -> Result { + let conf: DeckConfSchema11 = serde_json::from_slice(&input.config)?; + let mut conf: DeckConf = conf.into(); + self.with_col(|col| { + col.transact(None, |col| { + col.add_or_update_deck_config(&mut conf, input.preserve_usn_and_mtime)?; + Ok(pb::DeckConfigId { dcid: conf.id.0 }) + }) + }) + .map(Into::into) + } + + fn all_deck_config_legacy(&self, _input: pb::Empty) -> Result { + self.with_col(|col| { + let conf: Vec = col + .storage + .all_deck_config()? + .into_iter() + .map(Into::into) + .collect(); + serde_json::to_vec(&conf).map_err(Into::into) + }) + .map(Into::into) + } + + fn get_deck_config_legacy(&self, input: pb::DeckConfigId) -> Result { + self.with_col(|col| { + let conf = col.get_deck_config(input.into(), true)?.unwrap(); + let conf: DeckConfSchema11 = conf.into(); + Ok(serde_json::to_vec(&conf)?) + }) + .map(Into::into) + } + + fn new_deck_config_legacy(&self, _input: pb::Empty) -> Result { + serde_json::to_vec(&DeckConfSchema11::default()) + .map_err(Into::into) + .map(Into::into) + } + + fn remove_deck_config(&self, input: pb::DeckConfigId) -> Result { + self.with_col(|col| col.transact(None, |col| col.remove_deck_config(input.into()))) + .map(Into::into) + } +} diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index ecc65e4f7..91b591662 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -3,8 +3,10 @@ mod adding; mod card; +mod cardrendering; mod config; mod dbproxy; +mod deckconfig; mod decks; mod err; mod generic; @@ -14,38 +16,40 @@ mod progress; mod scheduler; mod search; mod sync; +mod tags; use self::{ + cardrendering::CardRenderingService, config::ConfigService, + deckconfig::DeckConfigService, decks::DecksService, notes::NotesService, notetypes::NoteTypesService, scheduler::SchedulingService, sync::{SyncService, SyncState}, + tags::TagsService, }; use crate::backend_proto::backend_service::Service as BackendService; use crate::{ backend::dbproxy::db_command_bytes, backend_proto as pb, - backend_proto::{AddOrUpdateDeckConfigLegacyIn, RenderedTemplateReplacement}, + backend_proto::RenderedTemplateReplacement, card::{Card, CardID}, collection::{open_collection, Collection}, - deckconf::{DeckConf, DeckConfSchema11}, err::{AnkiError, Result}, i18n::I18n, - latex::{extract_latex, extract_latex_expanding_clozes, ExtractedLatex}, log, log::default_logger, markdown::render_markdown, media::check::MediaChecker, media::MediaManager, notes::NoteID, - notetype::{CardTemplateSchema11, RenderCardOutput}, + notetype::RenderCardOutput, scheduler::timespan::{answer_button_time, time_span}, search::{concatenate_searches, replace_search_node, write_nodes, Node}, template::RenderedNode, - text::{extract_av_tags, sanitize_html_no_images, strip_av_tags, AVTag}, + text::sanitize_html_no_images, undo::UndoableOpKind, }; use fluent::FluentValue; @@ -108,113 +112,6 @@ impl BackendService for Backend { Ok(().into()) } - // card rendering - - fn extract_av_tags(&self, input: pb::ExtractAvTagsIn) -> Result { - let (text, tags) = extract_av_tags(&input.text, input.question_side); - let pt_tags = tags - .into_iter() - .map(|avtag| match avtag { - AVTag::SoundOrVideo(file) => pb::AvTag { - value: Some(pb::av_tag::Value::SoundOrVideo(file)), - }, - AVTag::TextToSpeech { - field_text, - lang, - voices, - other_args, - speed, - } => pb::AvTag { - value: Some(pb::av_tag::Value::Tts(pb::TtsTag { - field_text, - lang, - voices, - other_args, - speed, - })), - }, - }) - .collect(); - - Ok(pb::ExtractAvTagsOut { - text: text.into(), - av_tags: pt_tags, - }) - } - - fn extract_latex(&self, input: pb::ExtractLatexIn) -> Result { - let func = if input.expand_clozes { - extract_latex_expanding_clozes - } else { - extract_latex - }; - let (text, extracted) = func(&input.text, input.svg); - - Ok(pb::ExtractLatexOut { - text, - latex: extracted - .into_iter() - .map(|e: ExtractedLatex| pb::ExtractedLatex { - filename: e.fname, - latex_body: e.latex, - }) - .collect(), - }) - } - - fn get_empty_cards(&self, _input: pb::Empty) -> Result { - self.with_col(|col| { - let mut empty = col.empty_cards()?; - let report = col.empty_cards_report(&mut empty)?; - - let mut outnotes = vec![]; - for (_ntid, notes) in empty { - outnotes.extend(notes.into_iter().map(|e| { - pb::empty_cards_report::NoteWithEmptyCards { - note_id: e.nid.0, - will_delete_note: e.empty.len() == e.current_count, - card_ids: e.empty.into_iter().map(|(_ord, id)| id.0).collect(), - } - })) - } - Ok(pb::EmptyCardsReport { - report, - notes: outnotes, - }) - }) - } - - fn render_existing_card(&self, input: pb::RenderExistingCardIn) -> Result { - self.with_col(|col| { - col.render_existing_card(CardID(input.card_id), input.browser) - .map(Into::into) - }) - } - - fn render_uncommitted_card( - &self, - input: pb::RenderUncommittedCardIn, - ) -> Result { - let schema11: CardTemplateSchema11 = serde_json::from_slice(&input.template)?; - let template = schema11.into(); - let mut note = input - .note - .ok_or_else(|| AnkiError::invalid_input("missing note"))? - .into(); - let ord = input.card_ord as u16; - let fill_empty = input.fill_empty; - self.with_col(|col| { - col.render_uncommitted_card(&mut note, &template, ord, fill_empty) - .map(Into::into) - }) - } - - fn strip_av_tags(&self, input: pb::String) -> Result { - Ok(pb::String { - val: strip_av_tags(&input.val).into(), - }) - } - // searching //----------------------------------------------- @@ -384,57 +281,6 @@ impl BackendService for Backend { .map(Into::into) } - // deck config - //---------------------------------------------------- - - fn add_or_update_deck_config_legacy( - &self, - input: AddOrUpdateDeckConfigLegacyIn, - ) -> Result { - let conf: DeckConfSchema11 = serde_json::from_slice(&input.config)?; - let mut conf: DeckConf = conf.into(); - self.with_col(|col| { - col.transact(None, |col| { - col.add_or_update_deck_config(&mut conf, input.preserve_usn_and_mtime)?; - Ok(pb::DeckConfigId { dcid: conf.id.0 }) - }) - }) - .map(Into::into) - } - - fn all_deck_config_legacy(&self, _input: pb::Empty) -> Result { - self.with_col(|col| { - let conf: Vec = col - .storage - .all_deck_config()? - .into_iter() - .map(Into::into) - .collect(); - serde_json::to_vec(&conf).map_err(Into::into) - }) - .map(Into::into) - } - - fn get_deck_config_legacy(&self, input: pb::DeckConfigId) -> Result { - self.with_col(|col| { - let conf = col.get_deck_config(input.into(), true)?.unwrap(); - let conf: DeckConfSchema11 = conf.into(); - Ok(serde_json::to_vec(&conf)?) - }) - .map(Into::into) - } - - fn new_deck_config_legacy(&self, _input: pb::Empty) -> Result { - serde_json::to_vec(&DeckConfSchema11::default()) - .map_err(Into::into) - .map(Into::into) - } - - fn remove_deck_config(&self, input: pb::DeckConfigId) -> Result { - self.with_col(|col| col.transact(None, |col| col.remove_deck_config(input.into()))) - .map(Into::into) - } - // cards //------------------------------------------------------------------- @@ -605,59 +451,6 @@ impl BackendService for Backend { } Ok(text.into()) } - - // tags - //------------------------------------------------------------------- - - fn clear_unused_tags(&self, _input: pb::Empty) -> Result { - self.with_col(|col| col.transact(None, |col| col.clear_unused_tags().map(Into::into))) - } - - fn all_tags(&self, _input: pb::Empty) -> Result { - Ok(pb::StringList { - vals: self.with_col(|col| { - Ok(col - .storage - .all_tags()? - .into_iter() - .map(|t| t.name) - .collect()) - })?, - }) - } - - fn set_tag_expanded(&self, input: pb::SetTagExpandedIn) -> Result { - self.with_col(|col| { - col.transact(None, |col| { - col.set_tag_expanded(&input.name, input.expanded)?; - Ok(().into()) - }) - }) - } - - fn clear_tag(&self, tag: pb::String) -> Result { - self.with_col(|col| { - col.transact(None, |col| { - col.storage.clear_tag_and_children(tag.val.as_str())?; - Ok(().into()) - }) - }) - } - - fn tag_tree(&self, _input: pb::Empty) -> Result { - self.with_col(|col| col.tag_tree()) - } - - fn drag_drop_tags(&self, input: pb::DragDropTagsIn) -> Result { - let source_tags = input.source_tags; - let target_tag = if input.target_tag.is_empty() { - None - } else { - Some(input.target_tag) - }; - self.with_col(|col| col.drag_drop_tags(&source_tags, target_tag)) - .map(Into::into) - } } impl Backend { @@ -693,9 +486,14 @@ impl Backend { pb::ServiceIndex::Backend => BackendService::run_method(self, method, input), pb::ServiceIndex::Decks => DecksService::run_method(self, method, input), pb::ServiceIndex::Notes => NotesService::run_method(self, method, input), - pb::ServiceIndex::Notetypes => NoteTypesService::run_method(self, method, input), + pb::ServiceIndex::NoteTypes => NoteTypesService::run_method(self, method, input), pb::ServiceIndex::Config => ConfigService::run_method(self, method, input), pb::ServiceIndex::Sync => SyncService::run_method(self, method, input), + pb::ServiceIndex::Tags => TagsService::run_method(self, method, input), + pb::ServiceIndex::DeckConfig => DeckConfigService::run_method(self, method, input), + pb::ServiceIndex::CardRendering => { + CardRenderingService::run_method(self, method, input) + } }) .map_err(|err| { let backend_err = anki_error_to_proto_error(err, &self.i18n); diff --git a/rslib/src/backend/tags.rs b/rslib/src/backend/tags.rs new file mode 100644 index 000000000..8175cdf6c --- /dev/null +++ b/rslib/src/backend/tags.rs @@ -0,0 +1,58 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use super::Backend; +use crate::{backend_proto as pb, prelude::*}; +pub(super) use pb::tags_service::Service as TagsService; + +impl TagsService for Backend { + fn clear_unused_tags(&self, _input: pb::Empty) -> Result { + self.with_col(|col| col.transact(None, |col| col.clear_unused_tags().map(Into::into))) + } + + fn all_tags(&self, _input: pb::Empty) -> Result { + Ok(pb::StringList { + vals: self.with_col(|col| { + Ok(col + .storage + .all_tags()? + .into_iter() + .map(|t| t.name) + .collect()) + })?, + }) + } + + fn set_tag_expanded(&self, input: pb::SetTagExpandedIn) -> Result { + self.with_col(|col| { + col.transact(None, |col| { + col.set_tag_expanded(&input.name, input.expanded)?; + Ok(().into()) + }) + }) + } + + fn clear_tag(&self, tag: pb::String) -> Result { + self.with_col(|col| { + col.transact(None, |col| { + col.storage.clear_tag_and_children(tag.val.as_str())?; + Ok(().into()) + }) + }) + } + + fn tag_tree(&self, _input: pb::Empty) -> Result { + self.with_col(|col| col.tag_tree()) + } + + fn drag_drop_tags(&self, input: pb::DragDropTagsIn) -> Result { + let source_tags = input.source_tags; + let target_tag = if input.target_tag.is_empty() { + None + } else { + Some(input.target_tag) + }; + self.with_col(|col| col.drag_drop_tags(&source_tags, target_tag)) + .map(Into::into) + } +} From 378b1169771b94c87fda3f99ba24ff75b8d20140 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 11 Mar 2021 16:16:40 +1000 Subject: [PATCH 10/13] split out stats, media and search --- rslib/backend.proto | 23 +++-- rslib/src/backend/media.rs | 89 ++++++++++++++++++ rslib/src/backend/mod.rs | 181 ++---------------------------------- rslib/src/backend/search.rs | 77 ++++++++++++++- rslib/src/backend/stats.rs | 26 ++++++ 5 files changed, 211 insertions(+), 185 deletions(-) create mode 100644 rslib/src/backend/media.rs create mode 100644 rslib/src/backend/stats.rs diff --git a/rslib/backend.proto b/rslib/backend.proto index 9ed4e824d..a362810d8 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -88,6 +88,9 @@ enum ServiceIndex { SERVICE_INDEX_CARD_RENDERING = 6; SERVICE_INDEX_DECK_CONFIG = 7; SERVICE_INDEX_TAGS = 8; + SERVICE_INDEX_SEARCH = 9; + SERVICE_INDEX_STATS = 10; + SERVICE_INDEX_MEDIA = 11; SERVICE_INDEX_BACKEND = 99; } @@ -209,33 +212,33 @@ service TagsService { rpc DragDropTags(DragDropTagsIn) returns (Empty); } -service BackendService { - rpc LatestProgress(Empty) returns (Progress); - rpc SetWantsAbort(Empty) returns (Empty); - - // searching - +service SearchService { rpc BuildSearchString(SearchNode) returns (String); rpc SearchCards(SearchCardsIn) returns (SearchCardsOut); rpc SearchNotes(SearchNotesIn) returns (SearchNotesOut); rpc JoinSearchNodes(JoinSearchNodesIn) returns (String); rpc ReplaceSearchNode(ReplaceSearchNodeIn) returns (String); rpc FindAndReplace(FindAndReplaceIn) returns (UInt32); +} - // stats - +service StatsService { rpc CardStats(CardID) returns (String); rpc Graphs(GraphsIn) returns (GraphsOut); rpc GetGraphPreferences(Empty) returns (GraphPreferences); rpc SetGraphPreferences(GraphPreferences) returns (Empty); +} - // media - +service MediaService { rpc CheckMedia(Empty) returns (CheckMediaOut); rpc TrashMediaFiles(TrashMediaFilesIn) returns (Empty); rpc AddMediaFile(AddMediaFileIn) returns (String); rpc EmptyTrash(Empty) returns (Empty); rpc RestoreTrash(Empty) returns (Empty); +} + +service BackendService { + rpc LatestProgress(Empty) returns (Progress); + rpc SetWantsAbort(Empty) returns (Empty); // cards diff --git a/rslib/src/backend/media.rs b/rslib/src/backend/media.rs new file mode 100644 index 000000000..f36a92c60 --- /dev/null +++ b/rslib/src/backend/media.rs @@ -0,0 +1,89 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use super::{progress::Progress, Backend}; +use crate::{ + backend_proto as pb, + media::{check::MediaChecker, MediaManager}, + prelude::*, +}; +pub(super) use pb::media_service::Service as MediaService; + +impl MediaService for Backend { + // media + //----------------------------------------------- + + fn check_media(&self, _input: pb::Empty) -> Result { + let mut handler = self.new_progress_handler(); + let progress_fn = + move |progress| handler.update(Progress::MediaCheck(progress as u32), true); + self.with_col(|col| { + let mgr = MediaManager::new(&col.media_folder, &col.media_db)?; + col.transact(None, |ctx| { + let mut checker = MediaChecker::new(ctx, &mgr, progress_fn); + let mut output = checker.check()?; + + let report = checker.summarize_output(&mut output); + + Ok(pb::CheckMediaOut { + unused: output.unused, + missing: output.missing, + report, + have_trash: output.trash_count > 0, + }) + }) + }) + } + + fn trash_media_files(&self, input: pb::TrashMediaFilesIn) -> Result { + self.with_col(|col| { + let mgr = MediaManager::new(&col.media_folder, &col.media_db)?; + let mut ctx = mgr.dbctx(); + mgr.remove_files(&mut ctx, &input.fnames) + }) + .map(Into::into) + } + + fn add_media_file(&self, input: pb::AddMediaFileIn) -> Result { + self.with_col(|col| { + let mgr = MediaManager::new(&col.media_folder, &col.media_db)?; + let mut ctx = mgr.dbctx(); + Ok(mgr + .add_file(&mut ctx, &input.desired_name, &input.data)? + .to_string() + .into()) + }) + } + + fn empty_trash(&self, _input: pb::Empty) -> Result { + let mut handler = self.new_progress_handler(); + let progress_fn = + move |progress| handler.update(Progress::MediaCheck(progress as u32), true); + + self.with_col(|col| { + let mgr = MediaManager::new(&col.media_folder, &col.media_db)?; + col.transact(None, |ctx| { + let mut checker = MediaChecker::new(ctx, &mgr, progress_fn); + + checker.empty_trash() + }) + }) + .map(Into::into) + } + + fn restore_trash(&self, _input: pb::Empty) -> Result { + let mut handler = self.new_progress_handler(); + let progress_fn = + move |progress| handler.update(Progress::MediaCheck(progress as u32), true); + self.with_col(|col| { + let mgr = MediaManager::new(&col.media_folder, &col.media_db)?; + + col.transact(None, |ctx| { + let mut checker = MediaChecker::new(ctx, &mgr, progress_fn); + + checker.restore_trash() + }) + }) + .map(Into::into) + } +} diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 91b591662..bd48cb2bb 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -10,11 +10,13 @@ mod deckconfig; mod decks; mod err; mod generic; +mod media; mod notes; mod notetypes; mod progress; mod scheduler; mod search; +mod stats; mod sync; mod tags; @@ -23,9 +25,12 @@ use self::{ config::ConfigService, deckconfig::DeckConfigService, decks::DecksService, + media::MediaService, notes::NotesService, notetypes::NoteTypesService, scheduler::SchedulingService, + search::SearchService, + stats::StatsService, sync::{SyncService, SyncState}, tags::TagsService, }; @@ -42,12 +47,8 @@ use crate::{ log, log::default_logger, markdown::render_markdown, - media::check::MediaChecker, - media::MediaManager, - notes::NoteID, notetype::RenderCardOutput, scheduler::timespan::{answer_button_time, time_span}, - search::{concatenate_searches, replace_search_node, write_nodes, Node}, template::RenderedNode, text::sanitize_html_no_images, undo::UndoableOpKind, @@ -112,175 +113,6 @@ impl BackendService for Backend { Ok(().into()) } - // searching - //----------------------------------------------- - - fn build_search_string(&self, input: pb::SearchNode) -> Result { - let node: Node = input.try_into()?; - Ok(write_nodes(&node.into_node_list()).into()) - } - - fn search_cards(&self, input: pb::SearchCardsIn) -> Result { - self.with_col(|col| { - let order = input.order.unwrap_or_default().value.into(); - let cids = col.search_cards(&input.search, order)?; - Ok(pb::SearchCardsOut { - card_ids: cids.into_iter().map(|v| v.0).collect(), - }) - }) - } - - fn search_notes(&self, input: pb::SearchNotesIn) -> Result { - self.with_col(|col| { - let nids = col.search_notes(&input.search)?; - Ok(pb::SearchNotesOut { - note_ids: nids.into_iter().map(|v| v.0).collect(), - }) - }) - } - - fn join_search_nodes(&self, input: pb::JoinSearchNodesIn) -> Result { - let sep = input.joiner().into(); - let existing_nodes = { - let node: Node = input.existing_node.unwrap_or_default().try_into()?; - node.into_node_list() - }; - let additional_node = input.additional_node.unwrap_or_default().try_into()?; - Ok(concatenate_searches(sep, existing_nodes, additional_node).into()) - } - - fn replace_search_node(&self, input: pb::ReplaceSearchNodeIn) -> Result { - let existing = { - let node = input.existing_node.unwrap_or_default().try_into()?; - if let Node::Group(nodes) = node { - nodes - } else { - vec![node] - } - }; - let replacement = input.replacement_node.unwrap_or_default().try_into()?; - Ok(replace_search_node(existing, replacement).into()) - } - - fn find_and_replace(&self, input: pb::FindAndReplaceIn) -> Result { - let mut search = if input.regex { - input.search - } else { - regex::escape(&input.search) - }; - if !input.match_case { - search = format!("(?i){}", search); - } - let nids = input.nids.into_iter().map(NoteID).collect(); - let field_name = if input.field_name.is_empty() { - None - } else { - Some(input.field_name) - }; - let repl = input.replacement; - self.with_col(|col| { - col.find_and_replace(nids, &search, &repl, field_name) - .map(|cnt| pb::UInt32 { val: cnt as u32 }) - }) - } - // statistics - //----------------------------------------------- - - fn card_stats(&self, input: pb::CardId) -> Result { - self.with_col(|col| col.card_stats(input.into())) - .map(Into::into) - } - - fn graphs(&self, input: pb::GraphsIn) -> Result { - self.with_col(|col| col.graph_data_for_search(&input.search, input.days)) - } - - fn get_graph_preferences(&self, _input: pb::Empty) -> Result { - self.with_col(|col| col.get_graph_preferences()) - } - - fn set_graph_preferences(&self, input: pb::GraphPreferences) -> Result { - self.with_col(|col| col.set_graph_preferences(input)) - .map(Into::into) - } - - // media - //----------------------------------------------- - - fn check_media(&self, _input: pb::Empty) -> Result { - let mut handler = self.new_progress_handler(); - let progress_fn = - move |progress| handler.update(Progress::MediaCheck(progress as u32), true); - self.with_col(|col| { - let mgr = MediaManager::new(&col.media_folder, &col.media_db)?; - col.transact(None, |ctx| { - let mut checker = MediaChecker::new(ctx, &mgr, progress_fn); - let mut output = checker.check()?; - - let report = checker.summarize_output(&mut output); - - Ok(pb::CheckMediaOut { - unused: output.unused, - missing: output.missing, - report, - have_trash: output.trash_count > 0, - }) - }) - }) - } - - fn trash_media_files(&self, input: pb::TrashMediaFilesIn) -> Result { - self.with_col(|col| { - let mgr = MediaManager::new(&col.media_folder, &col.media_db)?; - let mut ctx = mgr.dbctx(); - mgr.remove_files(&mut ctx, &input.fnames) - }) - .map(Into::into) - } - - fn add_media_file(&self, input: pb::AddMediaFileIn) -> Result { - self.with_col(|col| { - let mgr = MediaManager::new(&col.media_folder, &col.media_db)?; - let mut ctx = mgr.dbctx(); - Ok(mgr - .add_file(&mut ctx, &input.desired_name, &input.data)? - .to_string() - .into()) - }) - } - - fn empty_trash(&self, _input: pb::Empty) -> Result { - let mut handler = self.new_progress_handler(); - let progress_fn = - move |progress| handler.update(Progress::MediaCheck(progress as u32), true); - - self.with_col(|col| { - let mgr = MediaManager::new(&col.media_folder, &col.media_db)?; - col.transact(None, |ctx| { - let mut checker = MediaChecker::new(ctx, &mgr, progress_fn); - - checker.empty_trash() - }) - }) - .map(Into::into) - } - - fn restore_trash(&self, _input: pb::Empty) -> Result { - let mut handler = self.new_progress_handler(); - let progress_fn = - move |progress| handler.update(Progress::MediaCheck(progress as u32), true); - self.with_col(|col| { - let mgr = MediaManager::new(&col.media_folder, &col.media_db)?; - - col.transact(None, |ctx| { - let mut checker = MediaChecker::new(ctx, &mgr, progress_fn); - - checker.restore_trash() - }) - }) - .map(Into::into) - } - // cards //------------------------------------------------------------------- @@ -494,6 +326,9 @@ impl Backend { pb::ServiceIndex::CardRendering => { CardRenderingService::run_method(self, method, input) } + pb::ServiceIndex::Media => MediaService::run_method(self, method, input), + pb::ServiceIndex::Stats => StatsService::run_method(self, method, input), + pb::ServiceIndex::Search => SearchService::run_method(self, method, input), }) .map_err(|err| { let backend_err = anki_error_to_proto_error(err, &self.i18n); diff --git a/rslib/src/backend/search.rs b/rslib/src/backend/search.rs index d0b134c2a..6bda494ac 100644 --- a/rslib/src/backend/search.rs +++ b/rslib/src/backend/search.rs @@ -4,6 +4,7 @@ use itertools::Itertools; use std::convert::{TryFrom, TryInto}; +use super::Backend; use crate::{ backend_proto as pb, backend_proto::{ @@ -12,11 +13,83 @@ use crate::{ config::SortKind, prelude::*, search::{ - parse_search, BoolSeparator, Node, PropertyKind, RatingKind, SearchNode, SortMode, - StateKind, TemplateKind, + concatenate_searches, parse_search, replace_search_node, write_nodes, BoolSeparator, Node, + PropertyKind, RatingKind, SearchNode, SortMode, StateKind, TemplateKind, }, text::escape_anki_wildcards, }; +pub(super) use pb::search_service::Service as SearchService; + +impl SearchService for Backend { + fn build_search_string(&self, input: pb::SearchNode) -> Result { + let node: Node = input.try_into()?; + Ok(write_nodes(&node.into_node_list()).into()) + } + + fn search_cards(&self, input: pb::SearchCardsIn) -> Result { + self.with_col(|col| { + let order = input.order.unwrap_or_default().value.into(); + let cids = col.search_cards(&input.search, order)?; + Ok(pb::SearchCardsOut { + card_ids: cids.into_iter().map(|v| v.0).collect(), + }) + }) + } + + fn search_notes(&self, input: pb::SearchNotesIn) -> Result { + self.with_col(|col| { + let nids = col.search_notes(&input.search)?; + Ok(pb::SearchNotesOut { + note_ids: nids.into_iter().map(|v| v.0).collect(), + }) + }) + } + + fn join_search_nodes(&self, input: pb::JoinSearchNodesIn) -> Result { + let sep = input.joiner().into(); + let existing_nodes = { + let node: Node = input.existing_node.unwrap_or_default().try_into()?; + node.into_node_list() + }; + let additional_node = input.additional_node.unwrap_or_default().try_into()?; + Ok(concatenate_searches(sep, existing_nodes, additional_node).into()) + } + + fn replace_search_node(&self, input: pb::ReplaceSearchNodeIn) -> Result { + let existing = { + let node = input.existing_node.unwrap_or_default().try_into()?; + if let Node::Group(nodes) = node { + nodes + } else { + vec![node] + } + }; + let replacement = input.replacement_node.unwrap_or_default().try_into()?; + Ok(replace_search_node(existing, replacement).into()) + } + + fn find_and_replace(&self, input: pb::FindAndReplaceIn) -> Result { + let mut search = if input.regex { + input.search + } else { + regex::escape(&input.search) + }; + if !input.match_case { + search = format!("(?i){}", search); + } + let nids = input.nids.into_iter().map(NoteID).collect(); + let field_name = if input.field_name.is_empty() { + None + } else { + Some(input.field_name) + }; + let repl = input.replacement; + self.with_col(|col| { + col.find_and_replace(nids, &search, &repl, field_name) + .map(|cnt| pb::UInt32 { val: cnt as u32 }) + }) + } +} impl TryFrom for Node { type Error = AnkiError; diff --git a/rslib/src/backend/stats.rs b/rslib/src/backend/stats.rs new file mode 100644 index 000000000..09c4f3f0a --- /dev/null +++ b/rslib/src/backend/stats.rs @@ -0,0 +1,26 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use super::Backend; +use crate::{backend_proto as pb, prelude::*}; +pub(super) use pb::stats_service::Service as StatsService; + +impl StatsService for Backend { + fn card_stats(&self, input: pb::CardId) -> Result { + self.with_col(|col| col.card_stats(input.into())) + .map(Into::into) + } + + fn graphs(&self, input: pb::GraphsIn) -> Result { + self.with_col(|col| col.graph_data_for_search(&input.search, input.days)) + } + + fn get_graph_preferences(&self, _input: pb::Empty) -> Result { + self.with_col(|col| col.get_graph_preferences()) + } + + fn set_graph_preferences(&self, input: pb::GraphPreferences) -> Result { + self.with_col(|col| col.set_graph_preferences(input)) + .map(Into::into) + } +} From 4bd120cc4b9f582034a40a51033548aff3304e56 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 11 Mar 2021 16:53:36 +1000 Subject: [PATCH 11/13] split out remaining rpc methods @david-allison-1 note this also changes the method index to start at 0 instead of 1 --- pylib/anki/_backend/genbackend.py | 4 +- rslib/backend.proto | 39 ++-- rslib/build/protobuf.rs | 2 +- rslib/src/backend/card.rs | 74 ++++++- rslib/src/backend/cardrendering.rs | 48 ++++- rslib/src/backend/collection.rs | 104 ++++++++++ rslib/src/backend/i18n.rs | 55 +++++ rslib/src/backend/mod.rs | 315 ++--------------------------- rslib/src/backend/scheduler/mod.rs | 9 + 9 files changed, 331 insertions(+), 319 deletions(-) create mode 100644 rslib/src/backend/collection.rs create mode 100644 rslib/src/backend/i18n.rs diff --git a/pylib/anki/_backend/genbackend.py b/pylib/anki/_backend/genbackend.py index 633fe1f88..64fff6da6 100755 --- a/pylib/anki/_backend/genbackend.py +++ b/pylib/anki/_backend/genbackend.py @@ -137,11 +137,11 @@ def render_method(service_idx, method_idx, method): {input_assign_outer}""" if method.name in SKIP_DECODE: - buf += f"""return self._run_command({service_idx}, {method_idx+1}, input) + buf += f"""return self._run_command({service_idx}, {method_idx}, input) """ else: buf += f"""output = pb.{method.output_type.name}() - output.ParseFromString(self._run_command({service_idx}, {method_idx+1}, input)) + output.ParseFromString(self._run_command({service_idx}, {method_idx}, input)) return output{single_field} """ diff --git a/rslib/backend.proto b/rslib/backend.proto index a362810d8..50a7db282 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -77,7 +77,7 @@ message DeckConfigID { /// while the protobuf descriptors expose the order services are defined in, /// that information is not available in prost, so we define an enum to make -/// sure all clients agree on the same service indices +/// sure all clients agree on the service index enum ServiceIndex { SERVICE_INDEX_SCHEDULING = 0; SERVICE_INDEX_DECKS = 1; @@ -91,7 +91,9 @@ enum ServiceIndex { SERVICE_INDEX_SEARCH = 9; SERVICE_INDEX_STATS = 10; SERVICE_INDEX_MEDIA = 11; - SERVICE_INDEX_BACKEND = 99; + SERVICE_INDEX_I18N = 12; + SERVICE_INDEX_COLLECTION = 13; + SERVICE_INDEX_CARDS = 14; } service SchedulingService { @@ -192,6 +194,7 @@ service CardRenderingService { rpc RenderExistingCard(RenderExistingCardIn) returns (RenderCardOut); rpc RenderUncommittedCard(RenderUncommittedCardIn) returns (RenderCardOut); rpc StripAVTags(String) returns (String); + rpc RenderMarkdown(RenderMarkdownIn) returns (String); } service DeckConfigService { @@ -236,32 +239,28 @@ service MediaService { rpc RestoreTrash(Empty) returns (Empty); } -service BackendService { - rpc LatestProgress(Empty) returns (Progress); - rpc SetWantsAbort(Empty) returns (Empty); - - // cards - - rpc GetCard(CardID) returns (Card); - rpc UpdateCard(UpdateCardIn) returns (Empty); - rpc RemoveCards(RemoveCardsIn) returns (Empty); - rpc SetDeck(SetDeckIn) returns (Empty); - - // collection +service I18nService { + rpc TranslateString(TranslateStringIn) returns (String); + rpc FormatTimespan(FormatTimespanIn) returns (String); + rpc I18nResources(Empty) returns (Json); +} +service CollectionService { rpc OpenCollection(OpenCollectionIn) returns (Empty); rpc CloseCollection(CloseCollectionIn) returns (Empty); rpc CheckDatabase(Empty) returns (CheckDatabaseOut); rpc GetUndoStatus(Empty) returns (UndoStatus); rpc Undo(Empty) returns (UndoStatus); rpc Redo(Empty) returns (UndoStatus); + rpc LatestProgress(Empty) returns (Progress); + rpc SetWantsAbort(Empty) returns (Empty); +} - // translation/messages/text manipulation - - rpc TranslateString(TranslateStringIn) returns (String); - rpc FormatTimespan(FormatTimespanIn) returns (String); - rpc I18nResources(Empty) returns (Json); - rpc RenderMarkdown(RenderMarkdownIn) returns (String); +service CardsService { + rpc GetCard(CardID) returns (Card); + rpc UpdateCard(UpdateCardIn) returns (Empty); + rpc RemoveCards(RemoveCardsIn) returns (Empty); + rpc SetDeck(SetDeckIn) returns (Empty); } // Protobuf stored in .anki2 files diff --git a/rslib/build/protobuf.rs b/rslib/build/protobuf.rs index 65c1c75ff..7ba44254d 100644 --- a/rslib/build/protobuf.rs +++ b/rslib/build/protobuf.rs @@ -21,7 +21,7 @@ pub trait Service { "{idx} => {{ let input = {input_type}::decode(input)?;\n", "let output = self.{rust_method}(input)?;\n", "let mut out_bytes = Vec::new(); output.encode(&mut out_bytes)?; Ok(out_bytes) }}, "), - idx = idx + 1, + idx = idx, input_type = method.input_type, rust_method = method.name ) diff --git a/rslib/src/backend/card.rs b/rslib/src/backend/card.rs index c859d8312..8172f3f56 100644 --- a/rslib/src/backend/card.rs +++ b/rslib/src/backend/card.rs @@ -1,13 +1,60 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use std::convert::TryFrom; +use std::convert::{TryFrom, TryInto}; +use super::Backend; use crate::prelude::*; use crate::{ backend_proto as pb, card::{CardQueue, CardType}, }; +pub(super) use pb::cards_service::Service as CardsService; + +impl CardsService for Backend { + fn get_card(&self, input: pb::CardId) -> Result { + self.with_col(|col| { + col.storage + .get_card(input.into()) + .and_then(|opt| opt.ok_or(AnkiError::NotFound)) + .map(Into::into) + }) + } + + fn update_card(&self, input: pb::UpdateCardIn) -> Result { + self.with_col(|col| { + let op = if input.skip_undo_entry { + None + } else { + Some(UndoableOpKind::UpdateCard) + }; + let mut card: Card = input.card.ok_or(AnkiError::NotFound)?.try_into()?; + col.update_card_with_op(&mut card, op) + }) + .map(Into::into) + } + + fn remove_cards(&self, input: pb::RemoveCardsIn) -> Result { + self.with_col(|col| { + col.transact(None, |col| { + col.remove_cards_and_orphaned_notes( + &input + .card_ids + .into_iter() + .map(Into::into) + .collect::>(), + )?; + Ok(().into()) + }) + }) + } + + fn set_deck(&self, input: pb::SetDeckIn) -> Result { + let cids: Vec<_> = input.card_ids.into_iter().map(CardID).collect(); + let deck_id = input.deck_id.into(); + self.with_col(|col| col.set_deck(&cids, deck_id).map(Into::into)) + } +} impl TryFrom for Card { type Error = AnkiError; @@ -39,3 +86,28 @@ impl TryFrom for Card { }) } } + +impl From for pb::Card { + fn from(c: Card) -> Self { + pb::Card { + id: c.id.0, + note_id: c.note_id.0, + deck_id: c.deck_id.0, + template_idx: c.template_idx as u32, + mtime_secs: c.mtime.0, + usn: c.usn.0, + ctype: c.ctype as u32, + queue: c.queue as i32, + due: c.due, + interval: c.interval, + ease_factor: c.ease_factor as u32, + reps: c.reps, + lapses: c.lapses, + remaining_steps: c.remaining_steps, + original_due: c.original_due, + original_deck_id: c.original_deck_id.0, + flags: c.flags as u32, + data: c.data, + } + } +} diff --git a/rslib/src/backend/cardrendering.rs b/rslib/src/backend/cardrendering.rs index 9bbe8b805..a245973af 100644 --- a/rslib/src/backend/cardrendering.rs +++ b/rslib/src/backend/cardrendering.rs @@ -5,9 +5,11 @@ use super::Backend; use crate::{ backend_proto as pb, latex::{extract_latex, extract_latex_expanding_clozes, ExtractedLatex}, - notetype::CardTemplateSchema11, + markdown::render_markdown, + notetype::{CardTemplateSchema11, RenderCardOutput}, prelude::*, - text::{extract_av_tags, strip_av_tags, AVTag}, + template::RenderedNode, + text::{extract_av_tags, sanitize_html_no_images, strip_av_tags, AVTag}, }; pub(super) use pb::cardrendering_service::Service as CardRenderingService; @@ -116,4 +118,46 @@ impl CardRenderingService for Backend { val: strip_av_tags(&input.val).into(), }) } + + fn render_markdown(&self, input: pb::RenderMarkdownIn) -> Result { + let mut text = render_markdown(&input.markdown); + if input.sanitize { + // currently no images + text = sanitize_html_no_images(&text); + } + Ok(text.into()) + } +} + +fn rendered_nodes_to_proto(nodes: Vec) -> Vec { + nodes + .into_iter() + .map(|n| pb::RenderedTemplateNode { + value: Some(rendered_node_to_proto(n)), + }) + .collect() +} + +fn rendered_node_to_proto(node: RenderedNode) -> pb::rendered_template_node::Value { + match node { + RenderedNode::Text { text } => pb::rendered_template_node::Value::Text(text), + RenderedNode::Replacement { + field_name, + current_text, + filters, + } => pb::rendered_template_node::Value::Replacement(pb::RenderedTemplateReplacement { + field_name, + current_text, + filters, + }), + } +} + +impl From for pb::RenderCardOut { + fn from(o: RenderCardOutput) -> Self { + pb::RenderCardOut { + question_nodes: rendered_nodes_to_proto(o.qnodes), + answer_nodes: rendered_nodes_to_proto(o.anodes), + } + } } diff --git a/rslib/src/backend/collection.rs b/rslib/src/backend/collection.rs new file mode 100644 index 000000000..6153d1f9d --- /dev/null +++ b/rslib/src/backend/collection.rs @@ -0,0 +1,104 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use super::{progress::Progress, Backend}; +use crate::{ + backend::progress::progress_to_proto, + backend_proto as pb, + collection::open_collection, + log::{self, default_logger}, + prelude::*, +}; +pub(super) use pb::collection_service::Service as CollectionService; +use slog::error; + +impl CollectionService for Backend { + fn latest_progress(&self, _input: pb::Empty) -> Result { + let progress = self.progress_state.lock().unwrap().last_progress; + Ok(progress_to_proto(progress, &self.i18n)) + } + + fn set_wants_abort(&self, _input: pb::Empty) -> Result { + self.progress_state.lock().unwrap().want_abort = true; + Ok(().into()) + } + + fn open_collection(&self, input: pb::OpenCollectionIn) -> Result { + let mut col = self.col.lock().unwrap(); + if col.is_some() { + return Err(AnkiError::CollectionAlreadyOpen); + } + + let mut path = input.collection_path.clone(); + path.push_str(".log"); + + let log_path = match input.log_path.as_str() { + "" => None, + path => Some(path), + }; + let logger = default_logger(log_path)?; + + let new_col = open_collection( + input.collection_path, + input.media_folder_path, + input.media_db_path, + self.server, + self.i18n.clone(), + logger, + )?; + + *col = Some(new_col); + + Ok(().into()) + } + + fn close_collection(&self, input: pb::CloseCollectionIn) -> Result { + self.abort_media_sync_and_wait(); + + let mut col = self.col.lock().unwrap(); + if col.is_none() { + return Err(AnkiError::CollectionNotOpen); + } + + let col_inner = col.take().unwrap(); + if input.downgrade_to_schema11 { + let log = log::terminal(); + if let Err(e) = col_inner.close(input.downgrade_to_schema11) { + error!(log, " failed: {:?}", e); + } + } + + Ok(().into()) + } + + fn check_database(&self, _input: pb::Empty) -> Result { + let mut handler = self.new_progress_handler(); + let progress_fn = move |progress, throttle| { + handler.update(Progress::DatabaseCheck(progress), throttle); + }; + self.with_col(|col| { + col.check_database(progress_fn) + .map(|problems| pb::CheckDatabaseOut { + problems: problems.to_i18n_strings(&col.i18n), + }) + }) + } + + fn get_undo_status(&self, _input: pb::Empty) -> Result { + self.with_col(|col| Ok(col.undo_status())) + } + + fn undo(&self, _input: pb::Empty) -> Result { + self.with_col(|col| { + col.undo()?; + Ok(col.undo_status()) + }) + } + + fn redo(&self, _input: pb::Empty) -> Result { + self.with_col(|col| { + col.redo()?; + Ok(col.undo_status()) + }) + } +} diff --git a/rslib/src/backend/i18n.rs b/rslib/src/backend/i18n.rs new file mode 100644 index 000000000..6a17a517e --- /dev/null +++ b/rslib/src/backend/i18n.rs @@ -0,0 +1,55 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use super::Backend; +use crate::{ + backend_proto as pb, + prelude::*, + scheduler::timespan::{answer_button_time, time_span}, +}; +use fluent::FluentValue; +pub(super) use pb::i18n_service::Service as I18nService; + +impl I18nService for Backend { + fn translate_string(&self, input: pb::TranslateStringIn) -> Result { + let key = match crate::fluent_proto::FluentString::from_i32(input.key) { + Some(key) => key, + None => return Ok("invalid key".to_string().into()), + }; + + let map = input + .args + .iter() + .map(|(k, v)| (k.as_str(), translate_arg_to_fluent_val(&v))) + .collect(); + + Ok(self.i18n.trn(key, map).into()) + } + + fn format_timespan(&self, input: pb::FormatTimespanIn) -> Result { + use pb::format_timespan_in::Context; + Ok(match input.context() { + Context::Precise => time_span(input.seconds, &self.i18n, true), + Context::Intervals => time_span(input.seconds, &self.i18n, false), + Context::AnswerButtons => answer_button_time(input.seconds, &self.i18n), + } + .into()) + } + + fn i18n_resources(&self, _input: pb::Empty) -> Result { + serde_json::to_vec(&self.i18n.resources_for_js()) + .map(Into::into) + .map_err(Into::into) + } +} + +fn translate_arg_to_fluent_val(arg: &pb::TranslateArgValue) -> FluentValue { + use pb::translate_arg_value::Value as V; + match &arg.value { + Some(val) => match val { + V::Str(s) => FluentValue::String(s.into()), + V::Number(f) => FluentValue::Number(f.into()), + }, + None => FluentValue::String("".into()), + } +} diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index bd48cb2bb..a13cb88f9 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -4,12 +4,14 @@ mod adding; mod card; mod cardrendering; +mod collection; mod config; mod dbproxy; mod deckconfig; mod decks; mod err; mod generic; +mod i18n; mod media; mod notes; mod notetypes; @@ -21,54 +23,42 @@ mod sync; mod tags; use self::{ + card::CardsService, cardrendering::CardRenderingService, + collection::CollectionService, config::ConfigService, deckconfig::DeckConfigService, decks::DecksService, + i18n::I18nService, media::MediaService, notes::NotesService, notetypes::NoteTypesService, + progress::ProgressState, scheduler::SchedulingService, search::SearchService, stats::StatsService, sync::{SyncService, SyncState}, tags::TagsService, }; -use crate::backend_proto::backend_service::Service as BackendService; use crate::{ backend::dbproxy::db_command_bytes, backend_proto as pb, - backend_proto::RenderedTemplateReplacement, - card::{Card, CardID}, - collection::{open_collection, Collection}, + collection::Collection, err::{AnkiError, Result}, i18n::I18n, log, - log::default_logger, - markdown::render_markdown, - notetype::RenderCardOutput, - scheduler::timespan::{answer_button_time, time_span}, - template::RenderedNode, - text::sanitize_html_no_images, - undo::UndoableOpKind, }; -use fluent::FluentValue; -use log::error; use once_cell::sync::OnceCell; -use progress::{AbortHandleSlot, Progress}; +use progress::AbortHandleSlot; use prost::Message; -use std::convert::TryInto; use std::{ result, sync::{Arc, Mutex}, }; use tokio::runtime::{self, Runtime}; -use self::{ - err::anki_error_to_proto_error, - progress::{progress_to_proto, ProgressState}, -}; +use self::err::anki_error_to_proto_error; pub struct Backend { col: Arc>>, @@ -80,8 +70,6 @@ pub struct Backend { state: Arc>, } -// fixme: move other items like runtime into here as well - #[derive(Default)] struct BackendState { sync: SyncState, @@ -102,189 +90,6 @@ pub fn init_backend(init_msg: &[u8]) -> std::result::Result { Ok(Backend::new(i18n, input.server)) } -impl BackendService for Backend { - fn latest_progress(&self, _input: pb::Empty) -> Result { - let progress = self.progress_state.lock().unwrap().last_progress; - Ok(progress_to_proto(progress, &self.i18n)) - } - - fn set_wants_abort(&self, _input: pb::Empty) -> Result { - self.progress_state.lock().unwrap().want_abort = true; - Ok(().into()) - } - - // cards - //------------------------------------------------------------------- - - fn get_card(&self, input: pb::CardId) -> Result { - self.with_col(|col| { - col.storage - .get_card(input.into()) - .and_then(|opt| opt.ok_or(AnkiError::NotFound)) - .map(Into::into) - }) - } - - fn update_card(&self, input: pb::UpdateCardIn) -> Result { - self.with_col(|col| { - let op = if input.skip_undo_entry { - None - } else { - Some(UndoableOpKind::UpdateCard) - }; - let mut card: Card = input.card.ok_or(AnkiError::NotFound)?.try_into()?; - col.update_card_with_op(&mut card, op) - }) - .map(Into::into) - } - - fn remove_cards(&self, input: pb::RemoveCardsIn) -> Result { - self.with_col(|col| { - col.transact(None, |col| { - col.remove_cards_and_orphaned_notes( - &input - .card_ids - .into_iter() - .map(Into::into) - .collect::>(), - )?; - Ok(().into()) - }) - }) - } - - fn set_deck(&self, input: pb::SetDeckIn) -> Result { - let cids: Vec<_> = input.card_ids.into_iter().map(CardID).collect(); - let deck_id = input.deck_id.into(); - self.with_col(|col| col.set_deck(&cids, deck_id).map(Into::into)) - } - - // collection - //------------------------------------------------------------------- - - fn open_collection(&self, input: pb::OpenCollectionIn) -> Result { - let mut col = self.col.lock().unwrap(); - if col.is_some() { - return Err(AnkiError::CollectionAlreadyOpen); - } - - let mut path = input.collection_path.clone(); - path.push_str(".log"); - - let log_path = match input.log_path.as_str() { - "" => None, - path => Some(path), - }; - let logger = default_logger(log_path)?; - - let new_col = open_collection( - input.collection_path, - input.media_folder_path, - input.media_db_path, - self.server, - self.i18n.clone(), - logger, - )?; - - *col = Some(new_col); - - Ok(().into()) - } - - fn close_collection(&self, input: pb::CloseCollectionIn) -> Result { - self.abort_media_sync_and_wait(); - - let mut col = self.col.lock().unwrap(); - if col.is_none() { - return Err(AnkiError::CollectionNotOpen); - } - - let col_inner = col.take().unwrap(); - if input.downgrade_to_schema11 { - let log = log::terminal(); - if let Err(e) = col_inner.close(input.downgrade_to_schema11) { - error!(log, " failed: {:?}", e); - } - } - - Ok(().into()) - } - - fn check_database(&self, _input: pb::Empty) -> Result { - let mut handler = self.new_progress_handler(); - let progress_fn = move |progress, throttle| { - handler.update(Progress::DatabaseCheck(progress), throttle); - }; - self.with_col(|col| { - col.check_database(progress_fn) - .map(|problems| pb::CheckDatabaseOut { - problems: problems.to_i18n_strings(&col.i18n), - }) - }) - } - - fn get_undo_status(&self, _input: pb::Empty) -> Result { - self.with_col(|col| Ok(col.undo_status())) - } - - fn undo(&self, _input: pb::Empty) -> Result { - self.with_col(|col| { - col.undo()?; - Ok(col.undo_status()) - }) - } - - fn redo(&self, _input: pb::Empty) -> Result { - self.with_col(|col| { - col.redo()?; - Ok(col.undo_status()) - }) - } - - // i18n/messages - //------------------------------------------------------------------- - - fn translate_string(&self, input: pb::TranslateStringIn) -> Result { - let key = match crate::fluent_proto::FluentString::from_i32(input.key) { - Some(key) => key, - None => return Ok("invalid key".to_string().into()), - }; - - let map = input - .args - .iter() - .map(|(k, v)| (k.as_str(), translate_arg_to_fluent_val(&v))) - .collect(); - - Ok(self.i18n.trn(key, map).into()) - } - - fn format_timespan(&self, input: pb::FormatTimespanIn) -> Result { - use pb::format_timespan_in::Context; - Ok(match input.context() { - Context::Precise => time_span(input.seconds, &self.i18n, true), - Context::Intervals => time_span(input.seconds, &self.i18n, false), - Context::AnswerButtons => answer_button_time(input.seconds, &self.i18n), - } - .into()) - } - - fn i18n_resources(&self, _input: pb::Empty) -> Result { - serde_json::to_vec(&self.i18n.resources_for_js()) - .map(Into::into) - .map_err(Into::into) - } - - fn render_markdown(&self, input: pb::RenderMarkdownIn) -> Result { - let mut text = render_markdown(&input.markdown); - if input.sanitize { - // currently no images - text = sanitize_html_no_images(&text); - } - Ok(text.into()) - } -} - impl Backend { pub fn new(i18n: I18n, server: bool) -> Backend { Backend { @@ -315,7 +120,6 @@ impl Backend { .ok_or_else(|| AnkiError::invalid_input("invalid service")) .and_then(|service| match service { pb::ServiceIndex::Scheduling => SchedulingService::run_method(self, method, input), - pb::ServiceIndex::Backend => BackendService::run_method(self, method, input), pb::ServiceIndex::Decks => DecksService::run_method(self, method, input), pb::ServiceIndex::Notes => NotesService::run_method(self, method, input), pb::ServiceIndex::NoteTypes => NoteTypesService::run_method(self, method, input), @@ -329,6 +133,9 @@ impl Backend { pb::ServiceIndex::Media => MediaService::run_method(self, method, input), pb::ServiceIndex::Stats => StatsService::run_method(self, method, input), pb::ServiceIndex::Search => SearchService::run_method(self, method, input), + pb::ServiceIndex::I18n => I18nService::run_method(self, method, input), + pb::ServiceIndex::Collection => CollectionService::run_method(self, method, input), + pb::ServiceIndex::Cards => CardsService::run_method(self, method, input), }) .map_err(|err| { let backend_err = anki_error_to_proto_error(err, &self.i18n); @@ -338,6 +145,15 @@ impl Backend { }) } + pub fn run_db_command_bytes(&self, input: &[u8]) -> std::result::Result, Vec> { + self.db_command(input).map_err(|err| { + let backend_err = anki_error_to_proto_error(err, &self.i18n); + let mut bytes = Vec::new(); + backend_err.encode(&mut bytes).unwrap(); + bytes + }) + } + /// If collection is open, run the provided closure while holding /// the mutex. /// If collection is not open, return an error. @@ -368,94 +184,7 @@ impl Backend { .clone() } - pub fn db_command(&self, input: &[u8]) -> Result> { + fn db_command(&self, input: &[u8]) -> Result> { self.with_col(|col| db_command_bytes(col, input)) } - - pub fn run_db_command_bytes(&self, input: &[u8]) -> std::result::Result, Vec> { - self.db_command(input).map_err(|err| { - let backend_err = anki_error_to_proto_error(err, &self.i18n); - let mut bytes = Vec::new(); - backend_err.encode(&mut bytes).unwrap(); - bytes - }) - } -} - -fn translate_arg_to_fluent_val(arg: &pb::TranslateArgValue) -> FluentValue { - use pb::translate_arg_value::Value as V; - match &arg.value { - Some(val) => match val { - V::Str(s) => FluentValue::String(s.into()), - V::Number(f) => FluentValue::Number(f.into()), - }, - None => FluentValue::String("".into()), - } -} - -fn rendered_nodes_to_proto(nodes: Vec) -> Vec { - nodes - .into_iter() - .map(|n| pb::RenderedTemplateNode { - value: Some(rendered_node_to_proto(n)), - }) - .collect() -} - -fn rendered_node_to_proto(node: RenderedNode) -> pb::rendered_template_node::Value { - match node { - RenderedNode::Text { text } => pb::rendered_template_node::Value::Text(text), - RenderedNode::Replacement { - field_name, - current_text, - filters, - } => pb::rendered_template_node::Value::Replacement(RenderedTemplateReplacement { - field_name, - current_text, - filters, - }), - } -} - -impl From for pb::RenderCardOut { - fn from(o: RenderCardOutput) -> Self { - pb::RenderCardOut { - question_nodes: rendered_nodes_to_proto(o.qnodes), - answer_nodes: rendered_nodes_to_proto(o.anodes), - } - } -} - -impl From for pb::Card { - fn from(c: Card) -> Self { - pb::Card { - id: c.id.0, - note_id: c.note_id.0, - deck_id: c.deck_id.0, - template_idx: c.template_idx as u32, - mtime_secs: c.mtime.0, - usn: c.usn.0, - ctype: c.ctype as u32, - queue: c.queue as i32, - due: c.due, - interval: c.interval, - ease_factor: c.ease_factor as u32, - reps: c.reps, - lapses: c.lapses, - remaining_steps: c.remaining_steps, - original_due: c.original_due, - original_deck_id: c.original_deck_id.0, - flags: c.flags as u32, - data: c.data, - } - } -} - -impl From for pb::SchedTimingTodayOut { - fn from(t: crate::scheduler::timing::SchedTimingToday) -> pb::SchedTimingTodayOut { - pb::SchedTimingTodayOut { - days_elapsed: t.days_elapsed, - next_day_at: t.next_day_at, - } - } } diff --git a/rslib/src/backend/scheduler/mod.rs b/rslib/src/backend/scheduler/mod.rs index 1e8a0a19c..63ffce7e5 100644 --- a/rslib/src/backend/scheduler/mod.rs +++ b/rslib/src/backend/scheduler/mod.rs @@ -175,3 +175,12 @@ impl SchedulingService for Backend { self.with_col(|col| col.get_queued_cards(input.fetch_limit, input.intraday_learning_only)) } } + +impl From for pb::SchedTimingTodayOut { + fn from(t: crate::scheduler::timing::SchedTimingToday) -> pb::SchedTimingTodayOut { + pb::SchedTimingTodayOut { + days_elapsed: t.days_elapsed, + next_day_at: t.next_day_at, + } + } +} From 984e2c2666bf29a040707eed127e7def21c46764 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 11 Mar 2021 18:54:30 +1000 Subject: [PATCH 12/13] add a separate 'rename deck' method --- pylib/anki/decks.py | 10 +++-- rslib/backend.proto | 6 +++ rslib/src/backend/decks.rs | 17 +++++++++ rslib/src/backend/generic.rs | 12 ------ rslib/src/decks/mod.rs | 74 ++++++++++++++++++++++++------------ rslib/src/decks/undo.rs | 6 +-- rslib/src/undo/ops.rs | 2 + 7 files changed, 84 insertions(+), 43 deletions(-) diff --git a/pylib/anki/decks.py b/pylib/anki/decks.py index 41be21647..4f9f336ff 100644 --- a/pylib/anki/decks.py +++ b/pylib/anki/decks.py @@ -250,11 +250,13 @@ class DeckManager: deck=to_json_bytes(g), preserve_usn_and_mtime=preserve_usn ) - def rename(self, g: Deck, newName: str) -> None: + def rename(self, deck: Union[Deck, int], new_name: str) -> None: "Rename deck prefix to NAME if not exists. Updates children." - g["name"] = newName - self.update(g, preserve_usn=False) - return + if isinstance(deck, int): + deck_id = deck + else: + deck_id = deck["id"] + self.col._backend.rename_deck(deck_id=deck_id, new_name=new_name) # Drag/drop ############################################################# diff --git a/rslib/backend.proto b/rslib/backend.proto index 50a7db282..f9bec2995 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -132,6 +132,7 @@ service DecksService { rpc NewDeckLegacy(Bool) returns (Json); rpc RemoveDeck(DeckID) returns (Empty); rpc DragDropDecks(DragDropDecksIn) returns (Empty); + rpc RenameDeck(RenameDeckIn) returns (Empty); } service NotesService { @@ -1448,3 +1449,8 @@ message DeckAndNotetype { int64 deck_id = 1; int64 notetype_id = 2; } + +message RenameDeckIn { + int64 deck_id = 1; + string new_name = 2; +} \ No newline at end of file diff --git a/rslib/src/backend/decks.rs b/rslib/src/backend/decks.rs index e072f777f..7b2c8d613 100644 --- a/rslib/src/backend/decks.rs +++ b/rslib/src/backend/decks.rs @@ -124,4 +124,21 @@ impl DecksService for Backend { self.with_col(|col| col.drag_drop_decks(&source_dids, target_did)) .map(Into::into) } + + fn rename_deck(&self, input: pb::RenameDeckIn) -> Result { + self.with_col(|col| col.rename_deck(input.deck_id.into(), &input.new_name)) + .map(Into::into) + } +} + +impl From for DeckID { + fn from(did: pb::DeckId) -> Self { + DeckID(did.did) + } +} + +impl From for pb::DeckId { + fn from(did: DeckID) -> Self { + pb::DeckId { did: did.0 } + } } diff --git a/rslib/src/backend/generic.rs b/rslib/src/backend/generic.rs index b3ea2be60..1919901a9 100644 --- a/rslib/src/backend/generic.rs +++ b/rslib/src/backend/generic.rs @@ -63,18 +63,6 @@ impl From for NoteTypeID { } } -impl From for DeckID { - fn from(did: pb::DeckId) -> Self { - DeckID(did.did) - } -} - -impl From for pb::DeckId { - fn from(did: DeckID) -> Self { - pb::DeckId { did: did.0 } - } -} - impl From for DeckConfID { fn from(dcid: pb::DeckConfigId) -> Self { DeckConfID(dcid.dcid) diff --git a/rslib/src/decks/mod.rs b/rslib/src/decks/mod.rs index 16cd714b5..9d8997dec 100644 --- a/rslib/src/decks/mod.rs +++ b/rslib/src/decks/mod.rs @@ -292,32 +292,41 @@ impl Collection { }) } - pub(crate) fn update_deck(&mut self, deck: &mut Deck) -> Result<()> { + pub fn update_deck(&mut self, deck: &mut Deck) -> Result<()> { self.transact(Some(UndoableOpKind::UpdateDeck), |col| { - let usn = col.usn()?; - col.prepare_deck_for_update(deck, usn)?; - deck.set_modified(usn); - if let Some(existing_deck) = col.storage.get_deck(deck.id)? { - let name_changed = existing_deck.name != deck.name; - if name_changed { - // match closest parent name - col.match_or_create_parents(deck, usn)?; - // rename children - col.rename_child_decks(&existing_deck, &deck.name, usn)?; - } - col.update_single_deck_undoable(deck, &existing_deck)?; - if name_changed { - // after updating, we need to ensure all grandparents exist, which may not be the case - // in the parent->child case - col.create_missing_parents(&deck.name, usn)?; - } - Ok(()) - } else { - Err(AnkiError::invalid_input("updating non-existent deck")) - } + let existing_deck = col.storage.get_deck(deck.id)?.ok_or(AnkiError::NotFound)?; + col.update_deck_inner(deck, existing_deck, col.usn()?) }) } + pub fn rename_deck(&mut self, did: DeckID, new_human_name: &str) -> Result<()> { + self.transact(Some(UndoableOpKind::RenameDeck), |col| { + let existing_deck = col.storage.get_deck(did)?.ok_or(AnkiError::NotFound)?; + let mut deck = existing_deck.clone(); + deck.name = human_deck_name_to_native(new_human_name); + col.update_deck_inner(&mut deck, existing_deck, col.usn()?) + }) + } + + fn update_deck_inner(&mut self, deck: &mut Deck, original: Deck, usn: Usn) -> Result<()> { + self.prepare_deck_for_update(deck, usn)?; + deck.set_modified(usn); + let name_changed = original.name != deck.name; + if name_changed { + // match closest parent name + self.match_or_create_parents(deck, usn)?; + // rename children + self.rename_child_decks(&original, &deck.name, usn)?; + } + self.update_single_deck_undoable(deck, original)?; + if name_changed { + // after updating, we need to ensure all grandparents exist, which may not be the case + // in the parent->child case + self.create_missing_parents(&deck.name, usn)?; + } + Ok(()) + } + /// Add/update a single deck when syncing/importing. Ensures name is unique /// & normalized, but does not check parents/children or update mtime /// (unless the name was changed). Caller must set up transaction. @@ -377,7 +386,7 @@ impl Collection { let new_name = format!("{}\x1f{}", new_name, child_only.join("\x1f")); child.name = new_name; child.set_modified(usn); - self.update_single_deck_undoable(&mut child, &original)?; + self.update_single_deck_undoable(&mut child, original)?; } Ok(()) @@ -600,7 +609,7 @@ impl Collection { deck.reset_stats_if_day_changed(today); mutator(&mut deck.common); deck.set_modified(usn); - self.update_single_deck_undoable(deck, &original) + self.update_single_deck_undoable(deck, original) } pub fn drag_drop_decks( @@ -761,6 +770,23 @@ mod test { col.add_or_update_deck(&mut middle)?; assert_eq!(middle.name, "other+"); + // public function takes human name + col.rename_deck(middle.id, "one::two")?; + assert_eq!( + sorted_names(&col), + vec![ + "Default", + "one", + "one::two", + "one::two::baz", + "one::two::baz2", + "other", + "quux", + "quux::foo", + "quux::foo::baz", + ] + ); + Ok(()) } diff --git a/rslib/src/decks/undo.rs b/rslib/src/decks/undo.rs index bd826becf..8d019ffb2 100644 --- a/rslib/src/decks/undo.rs +++ b/rslib/src/decks/undo.rs @@ -22,7 +22,7 @@ impl Collection { .storage .get_deck(deck.id)? .ok_or_else(|| AnkiError::invalid_input("deck disappeared"))?; - self.update_single_deck_undoable(&mut *deck, ¤t) + self.update_single_deck_undoable(&mut *deck, current) } UndoableDeckChange::Removed(deck) => self.restore_deleted_deck(*deck), UndoableDeckChange::GraveAdded(e) => self.remove_deck_grave(e.0, e.1), @@ -52,10 +52,10 @@ impl Collection { pub(super) fn update_single_deck_undoable( &mut self, deck: &mut Deck, - original: &Deck, + original: Deck, ) -> Result<()> { self.state.deck_cache.clear(); - self.save_undo(UndoableDeckChange::Updated(Box::new(original.clone()))); + self.save_undo(UndoableDeckChange::Updated(Box::new(original))); self.storage.update_deck(deck) } diff --git a/rslib/src/undo/ops.rs b/rslib/src/undo/ops.rs index d708ef943..23d9019d3 100644 --- a/rslib/src/undo/ops.rs +++ b/rslib/src/undo/ops.rs @@ -11,6 +11,7 @@ pub enum UndoableOpKind { Bury, RemoveDeck, RemoveNote, + RenameDeck, Suspend, UnburyUnsuspend, UpdateCard, @@ -35,6 +36,7 @@ impl Collection { UndoableOpKind::Bury => TR::StudyingBury, UndoableOpKind::RemoveDeck => TR::DecksDeleteDeck, UndoableOpKind::RemoveNote => TR::StudyingDeleteNote, + UndoableOpKind::RenameDeck => TR::ActionsRenameDeck, UndoableOpKind::Suspend => TR::StudyingSuspend, UndoableOpKind::UnburyUnsuspend => TR::UndoUnburyUnsuspend, UndoableOpKind::UpdateCard => TR::UndoUpdateCard, From e20c5ed9c5bbde1078e66970e8b31be1c6149afb Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 11 Mar 2021 19:54:29 +1000 Subject: [PATCH 13/13] deck drag&drop undo --- qt/aqt/deckbrowser.py | 20 ++++++++++++++------ qt/aqt/sidebar.py | 2 +- rslib/src/decks/mod.rs | 16 +++++++++++----- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/qt/aqt/deckbrowser.py b/qt/aqt/deckbrowser.py index 96d839fe3..6525f0d08 100644 --- a/qt/aqt/deckbrowser.py +++ b/qt/aqt/deckbrowser.py @@ -287,13 +287,21 @@ class DeckBrowser: self._renderPage(reuse=True) def _handle_drag_and_drop(self, source: int, target: int) -> None: - try: + def process() -> None: self.mw.col.decks.drag_drop_decks([source], target) - except Exception as e: - showWarning(str(e)) - return - gui_hooks.sidebar_should_refresh_decks() - self.show() + + def on_done(fut: Future) -> None: + try: + fut.result() + except Exception as e: + showWarning(str(e)) + return + + self.mw.update_undo_actions() + gui_hooks.sidebar_should_refresh_decks() + self.show() + + self.mw.taskman.with_progress(process, on_done) def ask_delete_deck(self, did: int) -> bool: deck = self.mw.col.decks.get(did) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 4db53abaa..1d5544250 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -480,9 +480,9 @@ class SidebarTreeView(QTreeView): return self.refresh() self.mw.deckBrowser.refresh() + self.mw.update_undo_actions() def on_save() -> None: - self.mw.checkpoint(tr(TR.ACTIONS_RENAME_DECK)) self.browser.model.beginReset() self.mw.taskman.with_progress( lambda: self.col.decks.drag_drop_decks(source_ids, target.id), on_done diff --git a/rslib/src/decks/mod.rs b/rslib/src/decks/mod.rs index 9d8997dec..ea0d5d71b 100644 --- a/rslib/src/decks/mod.rs +++ b/rslib/src/decks/mod.rs @@ -617,9 +617,8 @@ impl Collection { source_decks: &[DeckID], target: Option, ) -> Result<()> { - self.state.deck_cache.clear(); let usn = self.usn()?; - self.transact(None, |col| { + self.transact(Some(UndoableOpKind::RenameDeck), |col| { let target_deck; let mut target_name = None; if let Some(target) = target { @@ -634,18 +633,25 @@ impl Collection { for source in source_decks { if let Some(mut source) = col.storage.get_deck(*source)? { - let orig = source.clone(); let new_name = drag_drop_deck_name(&source.name, target_name); if new_name == source.name { continue; } + let orig = source.clone(); + + // this is basically update_deck_inner(), except: + // - we skip the normalization in prepare_for_update() + // - we skip the match_or_create_parents() step + + source.set_modified(usn); source.name = new_name; col.ensure_deck_name_unique(&mut source, usn)?; col.rename_child_decks(&orig, &source.name, usn)?; - source.set_modified(usn); - col.storage.update_deck(&source)?; + col.update_single_deck_undoable(&mut source, orig)?; + // after updating, we need to ensure all grandparents exist, which may not be the case // in the parent->child case + // FIXME: maybe we only need to do this once at the end of the loop? col.create_missing_parents(&source.name, usn)?; } }