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