diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 3f521aaa3..681accbd9 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -42,6 +42,7 @@ from aqt.mediasync import MediaSyncer from aqt.profiles import ProfileManager as ProfileManagerType from aqt.qt import * from aqt.qt import sip +from aqt.sync import sync_collection, sync_login from aqt.taskman import TaskManager from aqt.theme import theme_manager from aqt.utils import ( @@ -380,13 +381,9 @@ close the profile or restart Anki.""" self.taskman.run_in_background(downgrade, on_done) def loadProfile(self, onsuccess: Optional[Callable] = None) -> None: - self.maybeAutoSync() - if not self.loadCollection(): return - self.maybe_auto_sync_media() - self.pm.apply_profile_options() # show main window @@ -408,17 +405,16 @@ close the profile or restart Anki.""" self.handleImport(self.pendingImport) self.pendingImport = None gui_hooks.profile_did_open() - if onsuccess: - onsuccess() + + if onsuccess is None: + onsuccess = lambda: None + self.maybe_auto_sync_on_open_close(onsuccess) def unloadProfile(self, onsuccess: Callable) -> None: def callback(): self._unloadProfile() onsuccess() - # start media sync if not already running - self.maybe_auto_sync_media() - gui_hooks.profile_will_close() self.unloadCollection(callback) @@ -433,8 +429,6 @@ close the profile or restart Anki.""" # at this point there should be no windows left self._checkForUnclosedWidgets() - self.maybeAutoSync() - def _checkForUnclosedWidgets(self) -> None: for w in self.app.topLevelWidgets(): if w.isVisible(): @@ -520,13 +514,16 @@ close the profile or restart Anki.""" self.col.reopen() def unloadCollection(self, onsuccess: Callable) -> None: - def callback(): - self.setEnabled(False) + def after_sync(): self.media_syncer.show_diag_until_finished() self._unloadCollection() onsuccess() - self.closeAllWindows(callback) + def before_sync(): + self.setEnabled(False) + self.maybe_auto_sync_on_open_close(after_sync) + + self.closeAllWindows(before_sync) def _unloadCollection(self) -> None: if not self.col: @@ -869,52 +866,51 @@ title="%s" %s>%s""" % ( # Syncing ########################################################################## - # expects a current profile and a loaded collection; reloads - # collection after sync completes - def onSync(self): + def on_sync_button_clicked(self): if self.media_syncer.is_syncing(): self.media_syncer.show_sync_log() else: - self.temp_sync() - # self.unloadCollection(self._onSync) + auth = self.pm.sync_auth() + if not auth: + sync_login(self, self._sync_collection_and_media) + else: + self._sync_collection_and_media(lambda: None) - def _onSync(self): - self._sync() - if not self.loadCollection(): - return - self.media_syncer.start() + def _sync_collection_and_media(self, after_sync: Callable[[], None]): + "Caller should ensure auth available." + # start media sync if not already running + if not self.media_syncer.is_syncing(): + self.media_syncer.start() - # expects a current profile, but no collection loaded - def maybeAutoSync(self) -> None: - if ( - not self.pm.profile["syncKey"] - or not self.pm.profile["autoSync"] - or self.safeMode - or self.restoringBackup - ): - return + def on_collection_sync_finished(): + self.reset() + after_sync() - # ok to sync - self._sync() + sync_collection(self, on_done=on_collection_sync_finished) + + def maybe_auto_sync_on_open_close(self, after_sync: Callable[[], None]) -> None: + "If disabled, after_sync() is called immediately." + if self.can_auto_sync(): + self._sync_collection_and_media(after_sync) + else: + after_sync() def maybe_auto_sync_media(self) -> None: - if not self.pm.profile["autoSync"] or self.safeMode or self.restoringBackup: + if self.can_auto_sync(): return + # media_syncer takes care of media syncing preference check self.media_syncer.start() + def can_auto_sync(self) -> bool: + return (self.pm.auto_syncing_enabled() + and self.pm.sync_auth() + and not self.safeMode + and not self.restoringBackup) + + # legacy def _sync(self): - from aqt.sync import SyncManager - - self.state = "sync" - self.app.setQuitOnLastWindowClosed(False) - self.syncer = SyncManager(self, self.pm) - self.syncer.sync() - self.app.setQuitOnLastWindowClosed(True) - - def temp_sync(self): - from aqt.sync import sync - - sync(self) + pass + onSync = on_sync_button_clicked # Tools ########################################################################## @@ -940,7 +936,7 @@ title="%s" %s>%s""" % ( ("a", self.onAddCard), ("b", self.onBrowse), ("t", self.onStats), - ("y", self.onSync), + ("y", self.on_sync_button_clicked), ] self.applyShortcuts(globalShortcuts) diff --git a/qt/aqt/mediasync.py b/qt/aqt/mediasync.py index ab2dd8286..ac2acf4d2 100644 --- a/qt/aqt/mediasync.py +++ b/qt/aqt/mediasync.py @@ -62,7 +62,7 @@ class MediaSyncer: self._log_and_notify(tr(TR.SYNC_MEDIA_STARTING)) self._syncing = True - self._progress_timer = self.mw.progress.timer(1000, self._on_progress, True) + self._progress_timer = self.mw.progress.timer(1000, self._on_progress, False) gui_hooks.media_sync_did_start_or_stop(True) def run() -> None: diff --git a/qt/aqt/profiles.py b/qt/aqt/profiles.py index 7eaa01311..6707f0e38 100644 --- a/qt/aqt/profiles.py +++ b/qt/aqt/profiles.py @@ -618,6 +618,9 @@ create table if not exists profiles def media_syncing_enabled(self) -> bool: return self.profile["syncMedia"] + def auto_syncing_enabled(self) -> bool: + return self.profile["autoSync"] + def sync_auth(self) -> Optional[SyncAuth]: hkey = self.profile.get("syncKey") if not hkey: diff --git a/qt/aqt/sync.py b/qt/aqt/sync.py index fceae9dd7..d3900425a 100644 --- a/qt/aqt/sync.py +++ b/qt/aqt/sync.py @@ -28,6 +28,10 @@ from aqt.qt import ( ) from aqt.utils import askUser, askUserDialog, showText, showWarning, tr +# fixme: catch auth error in other routines, clear sync auth +# fixme: sync progress +# fixme: curDeck marking collection modified +# fixme: show progress immediately class FullSyncChoice(enum.Enum): CANCEL = 0 @@ -40,69 +44,73 @@ def get_sync_status(mw: aqt.main.AnkiQt, callback: Callable[[SyncOutput], None]) if not auth: return - def on_done(fut): + def on_future_done(fut): callback(fut.result()) - mw.taskman.run_in_background(lambda: mw.col.backend.sync_status(auth), on_done) + mw.taskman.run_in_background(lambda: mw.col.backend.sync_status(auth), on_future_done) -def sync(mw: aqt.main.AnkiQt) -> None: +def sync_collection(mw: aqt.main.AnkiQt, on_done: Callable[[], None]) -> None: auth = mw.pm.sync_auth() if not auth: - login(mw, on_success=lambda: sync(mw)) + sync_login(mw, on_success=lambda: sync_collection(mw)) return - def on_done(fut): + def on_future_done(fut): mw.col.db.begin() try: out: SyncOutput = fut.result() except InterruptedError: - return + return on_done() except Exception as e: showWarning(str(e)) - return + return on_done() mw.pm.set_host_number(out.host_number) if out.server_message: showText(out.server_message) if out.required == out.NO_CHANGES: # all done - return + return on_done() else: - full_sync(mw, out) + full_sync(mw, out, on_done) if not mw.col.basicCheck(): showWarning("Please use Tools>Check Database") - return + return on_done() mw.col.save(trx=False) mw.taskman.with_progress( lambda: mw.col.backend.sync_collection(auth), - on_done, + on_future_done, label=tr(TR.SYNC_CHECKING), ) -def full_sync(mw: aqt.main.AnkiQt, out: SyncOutput) -> None: +def full_sync( + mw: aqt.main.AnkiQt, out: SyncOutput, on_done: Callable[[], None] +) -> None: if out.required == out.FULL_DOWNLOAD: - confirm_full_download(mw) + confirm_full_download(mw, on_done) elif out.required == out.FULL_UPLOAD: - full_upload(mw) + full_upload(mw, on_done) else: choice = ask_user_to_decide_direction() if choice == FullSyncChoice.UPLOAD: - full_upload(mw) + full_upload(mw, on_done) elif choice == FullSyncChoice.DOWNLOAD: - full_download(mw) + full_download(mw, on_done) + else: + on_done() -def confirm_full_download(mw: aqt.main.AnkiQt) -> None: +def confirm_full_download(mw: aqt.main.AnkiQt, on_done: Callable[[], None]) -> None: # confirmation step required, as some users customize their notetypes # in an empty collection, then want to upload them if not askUser(tr(TR.SYNC_CONFIRM_EMPTY_DOWNLOAD)): - return + return on_done() else: - mw.closeAllWindows(lambda: full_download(mw)) + mw.closeAllWindows(lambda: full_download(mw, on_done)) def on_full_sync_timer(mw: aqt.main.AnkiQt) -> None: @@ -119,7 +127,7 @@ def on_full_sync_timer(mw: aqt.main.AnkiQt) -> None: mw.col.backend.abort_sync() -def full_download(mw: aqt.main.AnkiQt) -> None: +def full_download(mw: aqt.main.AnkiQt, on_done: Callable[[], None]) -> None: mw.col.close_for_full_sync() def on_timer(): @@ -129,7 +137,7 @@ def full_download(mw: aqt.main.AnkiQt) -> None: qconnect(timer.timeout, on_timer) timer.start(150) - def on_done(fut): + def on_future_done(fut): timer.stop() mw.col.reopen(after_full_sync=True) mw.reset() @@ -137,16 +145,16 @@ def full_download(mw: aqt.main.AnkiQt) -> None: fut.result() except Exception as e: showWarning(str(e)) - return + return on_done() mw.taskman.with_progress( lambda: mw.col.backend.full_download(mw.pm.sync_auth()), - on_done, + on_future_done, label=tr(TR.SYNC_DOWNLOADING_FROM_ANKIWEB), ) -def full_upload(mw: aqt.main.AnkiQt) -> None: +def full_upload(mw: aqt.main.AnkiQt, on_done: Callable[[], None]) -> None: mw.col.close_for_full_sync() def on_timer(): @@ -156,7 +164,7 @@ def full_upload(mw: aqt.main.AnkiQt) -> None: qconnect(timer.timeout, on_timer) timer.start(150) - def on_done(fut): + def on_future_done(fut): timer.stop() mw.col.reopen(after_full_sync=True) mw.reset() @@ -164,16 +172,15 @@ def full_upload(mw: aqt.main.AnkiQt) -> None: fut.result() except Exception as e: showWarning(str(e)) - return + return on_done() mw.taskman.with_progress( lambda: mw.col.backend.full_upload(mw.pm.sync_auth()), - on_done, + on_future_done, label=tr(TR.SYNC_UPLOADING_TO_ANKIWEB), ) - -def login( +def sync_login( mw: aqt.main.AnkiQt, on_success: Callable[[], None], username="", password="" ) -> None: while True: @@ -183,13 +190,13 @@ def login( if username and password: break - def on_done(fut): + def on_future_done(fut): try: auth = fut.result() except SyncError as e: if e.kind() == SyncErrorKind.AUTH_FAILED: showWarning(str(e)) - login(mw, on_success, username, password) + sync_login(mw, on_success, username, password) return except Exception as e: showWarning(str(e)) @@ -202,7 +209,8 @@ def login( on_success() mw.taskman.with_progress( - lambda: mw.col.backend.sync_login(username=username, password=password), on_done + lambda: mw.col.backend.sync_login(username=username, password=password), + on_future_done, ) diff --git a/qt/aqt/toolbar.py b/qt/aqt/toolbar.py index cec90d4b2..3067a580d 100644 --- a/qt/aqt/toolbar.py +++ b/qt/aqt/toolbar.py @@ -175,7 +175,7 @@ class Toolbar: self.mw.onStats() def _syncLinkHandler(self) -> None: - self.mw.onSync() + self.mw.on_sync_button_clicked() # HTML & CSS ######################################################################