From cb0ce4146faaeffb669bc9d1b8ebba5018ca8d96 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 4 Feb 2020 11:41:20 +1000 Subject: [PATCH] show spinner when media sync active, click to reveal dialog --- qt/aqt/__init__.py | 3 ++- qt/aqt/main.py | 33 ++++++++++++++++++++++++++- qt/aqt/mediasync.py | 35 +++++++++++++++++++++++----- qt/aqt/sync.py | 39 +------------------------------- qt/aqt/toolbar.py | 20 ++++++++++++++-- qt/aqt_data/web/imgs/refresh.svg | 1 + qt/ts/scss/toolbar.scss | 20 ++++++++++++++++ 7 files changed, 103 insertions(+), 48 deletions(-) create mode 100644 qt/aqt_data/web/imgs/refresh.svg diff --git a/qt/aqt/__init__.py b/qt/aqt/__init__.py index 7c79d9538..a2c3c10b7 100644 --- a/qt/aqt/__init__.py +++ b/qt/aqt/__init__.py @@ -64,7 +64,7 @@ except ImportError as e: from aqt import addcards, browser, editcurrent # isort:skip -from aqt import stats, about, preferences # isort:skip +from aqt import stats, about, preferences, mediasync # isort:skip class DialogManager: @@ -76,6 +76,7 @@ class DialogManager: "DeckStats": [stats.DeckStats, None], "About": [about.show, None], "Preferences": [preferences.Preferences, None], + "sync_log": [mediasync.MediaSyncDialog, None] } def open(self, name, *args): diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 10c1e93f3..a7b7bbd46 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -36,6 +36,7 @@ from anki.utils import devMode, ids2str, intTime, isMac, isWin, splitFields from aqt import gui_hooks from aqt.addons import DownloadLogEntry, check_and_prompt_for_updates, show_log_to_user from aqt.legacy import install_pylib_legacy +from aqt.mediasync import MediaSyncDialog, MediaSyncer from aqt.profiles import ProfileManager as ProfileManagerType from aqt.qt import * from aqt.qt import sip @@ -83,6 +84,7 @@ class AnkiQt(QMainWindow): self.opts = opts self.col: Optional[_Collection] = None self.taskman = TaskManager() + self.media_syncer = MediaSyncer(self.taskman, self._on_media_sync_start_stop) aqt.mw = self self.app = app self.pm = profileManager @@ -830,12 +832,16 @@ title="%s" %s>%s""" % ( # expects a current profile and a loaded collection; reloads # collection after sync completes def onSync(self): - self.unloadCollection(self._onSync) + if self.media_syncer.is_syncing(): + self._show_sync_log() + else: + self.unloadCollection(self._onSync) def _onSync(self): self._sync() if not self.loadCollection(): return + self._sync_media() # expects a current profile, but no collection loaded def maybeAutoSync(self) -> None: @@ -857,6 +863,31 @@ title="%s" %s>%s""" % ( self.syncer = SyncManager(self, self.pm) self.syncer.sync() + # fixme: self.pm.profile["syncMedia"] + # fixme: mediaSanity + # fixme: corruptMediaDB + # fixme: hkey + # fixme: shard + # fixme: dialog + # fixme: autosync +# elif evt == "mediaSanity": +# showWarning( +# _( +# """\ +# A problem occurred while syncing media. Please use Tools>Check Media, then \ +# sync again to correct the issue.""" +# ) +# ) + + def _sync_media(self): + self.media_syncer.start(self.col, self.pm.sync_key(), None) + + def _on_media_sync_start_stop(self): + self.toolbar.set_sync_active(self.media_syncer.is_syncing()) + + def _show_sync_log(self): + aqt.dialogs.open("sync_log", self, self.media_syncer) + # Tools ########################################################################## diff --git a/qt/aqt/mediasync.py b/qt/aqt/mediasync.py index aeba367d2..5ee572078 100644 --- a/qt/aqt/mediasync.py +++ b/qt/aqt/mediasync.py @@ -1,11 +1,13 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +from __future__ import annotations + import time from concurrent.futures import Future from copy import copy from dataclasses import dataclass -from typing import List, Optional, Union +from typing import List, Optional, Union, Callable import anki import aqt @@ -71,11 +73,12 @@ class LogEntryWithTime: class MediaSyncer: - def __init__(self, taskman: TaskManager): + def __init__(self, taskman: TaskManager, on_start_stop: Callable[[], None]): self._taskman = taskman self._sync_state: Optional[MediaSyncState] = None self._log: List[LogEntryWithTime] = [] self._want_stop = False + self._on_start_stop = on_start_stop hooks.rust_progress_callback.append(self._on_rust_progress) def _on_rust_progress(self, proceed: bool, progress: Progress) -> bool: @@ -111,6 +114,7 @@ class MediaSyncer: self._log_and_notify(SyncBegun()) self._sync_state = MediaSyncState() self._want_stop = False + self._on_start_stop() if shard is not None: shard_str = str(shard) @@ -134,6 +138,7 @@ class MediaSyncer: def _on_finished(self, future: Future) -> None: self._sync_state = None + self._on_start_stop() exc = future.exception() if exc is not None: @@ -150,10 +155,16 @@ class MediaSyncer: def abort(self) -> None: self._want_stop = True + def is_syncing(self) -> bool: + return self._sync_state is not None + class MediaSyncDialog(QDialog): - def __init__(self, parent: QWidget, syncer: MediaSyncer) -> None: - super().__init__(parent) + silentlyClose = True + + def __init__(self, mw: aqt.main.AnkiQt, syncer: MediaSyncer) -> None: + super().__init__(mw) + self.mw = mw self._syncer = syncer self.form = aqt.forms.synclog.Ui_Dialog() self.form.setupUi(self) @@ -166,6 +177,18 @@ class MediaSyncDialog(QDialog): self.form.plainTextEdit.setPlainText( "\n".join(self._entry_to_text(x) for x in syncer.entries()) ) + self.show() + + def reject(self): + aqt.dialogs.markClosed("sync_log") + QDialog.reject(self) + + def accept(self): + aqt.dialogs.markClosed("sync_log") + QDialog.accept(self) + + def reopen(self, *args): + self.show() def _on_abort(self, *args) -> None: self.form.plainTextEdit.appendPlainText( @@ -180,9 +203,9 @@ class MediaSyncDialog(QDialog): def _entry_to_text(self, entry: LogEntryWithTime): if isinstance(entry.entry, SyncBegun): - txt = _("Sync starting...") + txt = _("Media sync starting...") elif isinstance(entry.entry, SyncEnded): - txt = _("Sync complete.") + txt = _("Media sync complete.") elif isinstance(entry.entry, SyncAborted): txt = _("Aborted.") elif isinstance(entry.entry, MediaSyncState): diff --git a/qt/aqt/sync.py b/qt/aqt/sync.py index 78f7fe00b..dde492694 100644 --- a/qt/aqt/sync.py +++ b/qt/aqt/sync.py @@ -42,7 +42,6 @@ class SyncManager(QObject): self.pm.collectionPath(), self.pm.profile["syncKey"], auth=auth, - media=self.pm.profile["syncMedia"], hostNum=self.pm.profile.get("hostNum"), ) t._event.connect(self.onEvent) @@ -132,8 +131,6 @@ automatically.""" m = _("Downloading from AnkiWeb...") elif t == "sanity": m = _("Checking...") - elif t == "findMedia": - m = _("Checking media...") elif t == "upgradeRequired": showText( _( @@ -154,14 +151,6 @@ Please visit AnkiWeb, upgrade your deck, then try again.""" self._clockOff() elif evt == "checkFailed": self._checkFailed() - elif evt == "mediaSanity": - showWarning( - _( - """\ -A problem occurred while syncing media. Please use Tools>Check Media, then \ -sync again to correct the issue.""" - ) - ) elif evt == "noChanges": pass elif evt == "fullSync": @@ -358,12 +347,11 @@ class SyncThread(QThread): _event = pyqtSignal(str, str) progress_event = pyqtSignal(int, int) - def __init__(self, path, hkey, auth=None, media=True, hostNum=None): + def __init__(self, path, hkey, auth=None, hostNum=None): QThread.__init__(self) self.path = path self.hkey = hkey self.auth = auth - self.media = media self.hostNum = hostNum self._abort = 0 # 1=flagged, 2=aborting @@ -475,8 +463,6 @@ class SyncThread(QThread): self.syncMsg = self.client.syncMsg self.uname = self.client.uname self.hostNum = self.client.hostNum - # then move on to media sync - self._syncMedia() def _fullSync(self): # tell the calling thread we need a decision on sync direction, and @@ -505,29 +491,6 @@ class SyncThread(QThread): if "sync cancelled" in str(e): return raise - # reopen db and move on to media sync - self.col.reopen() - self._syncMedia() - - def _syncMedia(self): - if not self.media: - return - self.server = RemoteMediaServer( - self.col, self.hkey, self.server.client, hostNum=self.hostNum - ) - self.client = MediaSyncer(self.col, self.server) - try: - ret = self.client.sync() - except Exception as e: - if "sync cancelled" in str(e): - return - raise - if ret == "noChanges": - self.fireEvent("noMediaChanges") - elif ret == "sanityCheckFailed" or ret == "corruptMediaDB": - self.fireEvent("mediaSanity") - else: - self.fireEvent("mediaSuccess") def fireEvent(self, cmd, arg=""): self._event.emit(cmd, arg) diff --git a/qt/aqt/toolbar.py b/qt/aqt/toolbar.py index 088a371e0..2ffcb282f 100644 --- a/qt/aqt/toolbar.py +++ b/qt/aqt/toolbar.py @@ -62,9 +62,9 @@ class Toolbar: ["add", _("Add"), _("Shortcut key: %s") % "A"], ["browse", _("Browse"), _("Shortcut key: %s") % "B"], ["stats", _("Stats"), _("Shortcut key: %s") % "T"], - ["sync", _("Sync"), _("Shortcut key: %s") % "Y"], ] - return self._linkHTML(links) + + return self._linkHTML(links) + self._sync_link() def _linkHTML(self, links): buf = "" @@ -78,6 +78,22 @@ class Toolbar: ) return buf + def _sync_link(self) -> str: + name = _("Sync") + title = _("Shortcut key: %s") % "Y" + label = "sync" + return f""" +{name} + +""" + + def set_sync_active(self, active: bool) -> None: + if active: + meth = "addClass" + else: + meth = "removeClass" + self.web.eval(f"$('#sync-spinner').{meth}('spin')") + # Link handling ###################################################################### diff --git a/qt/aqt_data/web/imgs/refresh.svg b/qt/aqt_data/web/imgs/refresh.svg new file mode 100644 index 000000000..437fcbe0d --- /dev/null +++ b/qt/aqt_data/web/imgs/refresh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/qt/ts/scss/toolbar.scss b/qt/ts/scss/toolbar.scss index 8ebcff3ea..dda7d27a4 100644 --- a/qt/ts/scss/toolbar.scss +++ b/qt/ts/scss/toolbar.scss @@ -50,3 +50,23 @@ body { .isMac.nightMode #header { border-bottom-color: vars.$night-frame-bg; } + +@keyframes spin { + 0% {-webkit-transform: rotate(0deg);} + 100% {-webkit-transform: rotate(360deg);} +} + +.spin { + animation: spin; + animation-duration: 2s; + animation-iteration-count: infinite; + display: inline-block; + visibility: visible !important; +} + +#sync-spinner { + width: 16px; + height: 16px; + margin-bottom: -3px; + visibility: hidden; +} \ No newline at end of file