show spinner when media sync active, click to reveal dialog

This commit is contained in:
Damien Elmes 2020-02-04 11:41:20 +10:00
parent ea4de9a6de
commit cb0ce4146f
7 changed files with 103 additions and 48 deletions

View File

@ -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):

View File

@ -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</button>""" % (
# 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</button>""" % (
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
##########################################################################

View File

@ -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):

View File

@ -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)

View File

@ -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"""
<a class=hitem tabindex="-1" aria-label="{name}" title="{title}" href=# onclick="return pycmd('{label}')">{name}
<img id=sync-spinner src='/_anki/imgs/refresh.svg'>
</a>"""
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
######################################################################

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;"><g id="Layer-1" serif:id="Layer 1"><g><path d="M18.004,13.502l10.741,0l0,-10.83l-10.741,10.83Z" style="fill:#808080;fill-rule:nonzero;stroke:#808080;stroke-width:1px;"/><path d="M24.912,19.779c-1.384,3.394 -4.584,5.486 -8.33,5.486c-5.018,0 -9.093,-4.131 -9.093,-9.149c0,-5.018 4.121,-9.137 9.139,-9.137c2.516,0 4.81,1.026 6.464,2.687l2.604,-3.041c-2.355,-2.296 -5.53,-3.716 -9.079,-3.716c-7.216,0 -13.048,5.85 -13.048,13.066c0,7.216 5.469,13.116 12.685,13.116c5.929,0 10.671,-4.221 12.177,-9.312l-3.519,0Z" style="fill:#808080;fill-rule:nonzero;stroke:#808080;stroke-width:1px;stroke-linejoin:miter;stroke-miterlimit:10;"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -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;
}