update auto-sync code

This commit is contained in:
Damien Elmes 2020-05-31 10:53:54 +10:00
parent 058ff1b71a
commit 7e221f0acf
5 changed files with 90 additions and 83 deletions

View file

@ -42,6 +42,7 @@ from aqt.mediasync import MediaSyncer
from aqt.profiles import ProfileManager as ProfileManagerType from aqt.profiles import ProfileManager as ProfileManagerType
from aqt.qt import * from aqt.qt import *
from aqt.qt import sip from aqt.qt import sip
from aqt.sync import sync_collection, sync_login
from aqt.taskman import TaskManager from aqt.taskman import TaskManager
from aqt.theme import theme_manager from aqt.theme import theme_manager
from aqt.utils import ( from aqt.utils import (
@ -380,13 +381,9 @@ close the profile or restart Anki."""
self.taskman.run_in_background(downgrade, on_done) self.taskman.run_in_background(downgrade, on_done)
def loadProfile(self, onsuccess: Optional[Callable] = None) -> None: def loadProfile(self, onsuccess: Optional[Callable] = None) -> None:
self.maybeAutoSync()
if not self.loadCollection(): if not self.loadCollection():
return return
self.maybe_auto_sync_media()
self.pm.apply_profile_options() self.pm.apply_profile_options()
# show main window # show main window
@ -408,17 +405,16 @@ close the profile or restart Anki."""
self.handleImport(self.pendingImport) self.handleImport(self.pendingImport)
self.pendingImport = None self.pendingImport = None
gui_hooks.profile_did_open() 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 unloadProfile(self, onsuccess: Callable) -> None:
def callback(): def callback():
self._unloadProfile() self._unloadProfile()
onsuccess() onsuccess()
# start media sync if not already running
self.maybe_auto_sync_media()
gui_hooks.profile_will_close() gui_hooks.profile_will_close()
self.unloadCollection(callback) self.unloadCollection(callback)
@ -433,8 +429,6 @@ close the profile or restart Anki."""
# at this point there should be no windows left # at this point there should be no windows left
self._checkForUnclosedWidgets() self._checkForUnclosedWidgets()
self.maybeAutoSync()
def _checkForUnclosedWidgets(self) -> None: def _checkForUnclosedWidgets(self) -> None:
for w in self.app.topLevelWidgets(): for w in self.app.topLevelWidgets():
if w.isVisible(): if w.isVisible():
@ -520,13 +514,16 @@ close the profile or restart Anki."""
self.col.reopen() self.col.reopen()
def unloadCollection(self, onsuccess: Callable) -> None: def unloadCollection(self, onsuccess: Callable) -> None:
def callback(): def after_sync():
self.setEnabled(False)
self.media_syncer.show_diag_until_finished() self.media_syncer.show_diag_until_finished()
self._unloadCollection() self._unloadCollection()
onsuccess() 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: def _unloadCollection(self) -> None:
if not self.col: if not self.col:
@ -869,52 +866,51 @@ title="%s" %s>%s</button>""" % (
# Syncing # Syncing
########################################################################## ##########################################################################
# expects a current profile and a loaded collection; reloads def on_sync_button_clicked(self):
# collection after sync completes
def onSync(self):
if self.media_syncer.is_syncing(): if self.media_syncer.is_syncing():
self.media_syncer.show_sync_log() self.media_syncer.show_sync_log()
else: else:
self.temp_sync() auth = self.pm.sync_auth()
# self.unloadCollection(self._onSync) if not auth:
sync_login(self, self._sync_collection_and_media)
else:
self._sync_collection_and_media(lambda: None)
def _onSync(self): def _sync_collection_and_media(self, after_sync: Callable[[], None]):
self._sync() "Caller should ensure auth available."
if not self.loadCollection(): # start media sync if not already running
return if not self.media_syncer.is_syncing():
self.media_syncer.start() self.media_syncer.start()
# expects a current profile, but no collection loaded def on_collection_sync_finished():
def maybeAutoSync(self) -> None: self.reset()
if ( after_sync()
not self.pm.profile["syncKey"]
or not self.pm.profile["autoSync"]
or self.safeMode
or self.restoringBackup
):
return
# ok to sync sync_collection(self, on_done=on_collection_sync_finished)
self._sync()
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: 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 return
# media_syncer takes care of media syncing preference check
self.media_syncer.start() 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): def _sync(self):
from aqt.sync import SyncManager pass
onSync = on_sync_button_clicked
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)
# Tools # Tools
########################################################################## ##########################################################################
@ -940,7 +936,7 @@ title="%s" %s>%s</button>""" % (
("a", self.onAddCard), ("a", self.onAddCard),
("b", self.onBrowse), ("b", self.onBrowse),
("t", self.onStats), ("t", self.onStats),
("y", self.onSync), ("y", self.on_sync_button_clicked),
] ]
self.applyShortcuts(globalShortcuts) self.applyShortcuts(globalShortcuts)

View file

@ -62,7 +62,7 @@ class MediaSyncer:
self._log_and_notify(tr(TR.SYNC_MEDIA_STARTING)) self._log_and_notify(tr(TR.SYNC_MEDIA_STARTING))
self._syncing = True 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) gui_hooks.media_sync_did_start_or_stop(True)
def run() -> None: def run() -> None:

View file

@ -618,6 +618,9 @@ create table if not exists profiles
def media_syncing_enabled(self) -> bool: def media_syncing_enabled(self) -> bool:
return self.profile["syncMedia"] return self.profile["syncMedia"]
def auto_syncing_enabled(self) -> bool:
return self.profile["autoSync"]
def sync_auth(self) -> Optional[SyncAuth]: def sync_auth(self) -> Optional[SyncAuth]:
hkey = self.profile.get("syncKey") hkey = self.profile.get("syncKey")
if not hkey: if not hkey:

View file

@ -28,6 +28,10 @@ from aqt.qt import (
) )
from aqt.utils import askUser, askUserDialog, showText, showWarning, tr 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): class FullSyncChoice(enum.Enum):
CANCEL = 0 CANCEL = 0
@ -40,69 +44,73 @@ def get_sync_status(mw: aqt.main.AnkiQt, callback: Callable[[SyncOutput], None])
if not auth: if not auth:
return return
def on_done(fut): def on_future_done(fut):
callback(fut.result()) 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() auth = mw.pm.sync_auth()
if not auth: if not auth:
login(mw, on_success=lambda: sync(mw)) sync_login(mw, on_success=lambda: sync_collection(mw))
return return
def on_done(fut): def on_future_done(fut):
mw.col.db.begin() mw.col.db.begin()
try: try:
out: SyncOutput = fut.result() out: SyncOutput = fut.result()
except InterruptedError: except InterruptedError:
return return on_done()
except Exception as e: except Exception as e:
showWarning(str(e)) showWarning(str(e))
return return on_done()
mw.pm.set_host_number(out.host_number) mw.pm.set_host_number(out.host_number)
if out.server_message: if out.server_message:
showText(out.server_message) showText(out.server_message)
if out.required == out.NO_CHANGES: if out.required == out.NO_CHANGES:
# all done # all done
return return on_done()
else: else:
full_sync(mw, out) full_sync(mw, out, on_done)
if not mw.col.basicCheck(): if not mw.col.basicCheck():
showWarning("Please use Tools>Check Database") showWarning("Please use Tools>Check Database")
return return on_done()
mw.col.save(trx=False) mw.col.save(trx=False)
mw.taskman.with_progress( mw.taskman.with_progress(
lambda: mw.col.backend.sync_collection(auth), lambda: mw.col.backend.sync_collection(auth),
on_done, on_future_done,
label=tr(TR.SYNC_CHECKING), 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: if out.required == out.FULL_DOWNLOAD:
confirm_full_download(mw) confirm_full_download(mw, on_done)
elif out.required == out.FULL_UPLOAD: elif out.required == out.FULL_UPLOAD:
full_upload(mw) full_upload(mw, on_done)
else: else:
choice = ask_user_to_decide_direction() choice = ask_user_to_decide_direction()
if choice == FullSyncChoice.UPLOAD: if choice == FullSyncChoice.UPLOAD:
full_upload(mw) full_upload(mw, on_done)
elif choice == FullSyncChoice.DOWNLOAD: 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 # confirmation step required, as some users customize their notetypes
# in an empty collection, then want to upload them # in an empty collection, then want to upload them
if not askUser(tr(TR.SYNC_CONFIRM_EMPTY_DOWNLOAD)): if not askUser(tr(TR.SYNC_CONFIRM_EMPTY_DOWNLOAD)):
return return on_done()
else: 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: 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() 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() mw.col.close_for_full_sync()
def on_timer(): def on_timer():
@ -129,7 +137,7 @@ def full_download(mw: aqt.main.AnkiQt) -> None:
qconnect(timer.timeout, on_timer) qconnect(timer.timeout, on_timer)
timer.start(150) timer.start(150)
def on_done(fut): def on_future_done(fut):
timer.stop() timer.stop()
mw.col.reopen(after_full_sync=True) mw.col.reopen(after_full_sync=True)
mw.reset() mw.reset()
@ -137,16 +145,16 @@ def full_download(mw: aqt.main.AnkiQt) -> None:
fut.result() fut.result()
except Exception as e: except Exception as e:
showWarning(str(e)) showWarning(str(e))
return return on_done()
mw.taskman.with_progress( mw.taskman.with_progress(
lambda: mw.col.backend.full_download(mw.pm.sync_auth()), lambda: mw.col.backend.full_download(mw.pm.sync_auth()),
on_done, on_future_done,
label=tr(TR.SYNC_DOWNLOADING_FROM_ANKIWEB), 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() mw.col.close_for_full_sync()
def on_timer(): def on_timer():
@ -156,7 +164,7 @@ def full_upload(mw: aqt.main.AnkiQt) -> None:
qconnect(timer.timeout, on_timer) qconnect(timer.timeout, on_timer)
timer.start(150) timer.start(150)
def on_done(fut): def on_future_done(fut):
timer.stop() timer.stop()
mw.col.reopen(after_full_sync=True) mw.col.reopen(after_full_sync=True)
mw.reset() mw.reset()
@ -164,16 +172,15 @@ def full_upload(mw: aqt.main.AnkiQt) -> None:
fut.result() fut.result()
except Exception as e: except Exception as e:
showWarning(str(e)) showWarning(str(e))
return return on_done()
mw.taskman.with_progress( mw.taskman.with_progress(
lambda: mw.col.backend.full_upload(mw.pm.sync_auth()), lambda: mw.col.backend.full_upload(mw.pm.sync_auth()),
on_done, on_future_done,
label=tr(TR.SYNC_UPLOADING_TO_ANKIWEB), label=tr(TR.SYNC_UPLOADING_TO_ANKIWEB),
) )
def sync_login(
def login(
mw: aqt.main.AnkiQt, on_success: Callable[[], None], username="", password="" mw: aqt.main.AnkiQt, on_success: Callable[[], None], username="", password=""
) -> None: ) -> None:
while True: while True:
@ -183,13 +190,13 @@ def login(
if username and password: if username and password:
break break
def on_done(fut): def on_future_done(fut):
try: try:
auth = fut.result() auth = fut.result()
except SyncError as e: except SyncError as e:
if e.kind() == SyncErrorKind.AUTH_FAILED: if e.kind() == SyncErrorKind.AUTH_FAILED:
showWarning(str(e)) showWarning(str(e))
login(mw, on_success, username, password) sync_login(mw, on_success, username, password)
return return
except Exception as e: except Exception as e:
showWarning(str(e)) showWarning(str(e))
@ -202,7 +209,8 @@ def login(
on_success() on_success()
mw.taskman.with_progress( 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,
) )

View file

@ -175,7 +175,7 @@ class Toolbar:
self.mw.onStats() self.mw.onStats()
def _syncLinkHandler(self) -> None: def _syncLinkHandler(self) -> None:
self.mw.onSync() self.mw.on_sync_button_clicked()
# HTML & CSS # HTML & CSS
###################################################################### ######################################################################