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 addcards, browser, editcurrent # isort:skip
from aqt import stats, about, preferences # isort:skip from aqt import stats, about, preferences, mediasync # isort:skip
class DialogManager: class DialogManager:
@ -76,6 +76,7 @@ class DialogManager:
"DeckStats": [stats.DeckStats, None], "DeckStats": [stats.DeckStats, None],
"About": [about.show, None], "About": [about.show, None],
"Preferences": [preferences.Preferences, None], "Preferences": [preferences.Preferences, None],
"sync_log": [mediasync.MediaSyncDialog, None]
} }
def open(self, name, *args): 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 import gui_hooks
from aqt.addons import DownloadLogEntry, check_and_prompt_for_updates, show_log_to_user from aqt.addons import DownloadLogEntry, check_and_prompt_for_updates, show_log_to_user
from aqt.legacy import install_pylib_legacy from aqt.legacy import install_pylib_legacy
from aqt.mediasync import MediaSyncDialog, 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
@ -83,6 +84,7 @@ class AnkiQt(QMainWindow):
self.opts = opts self.opts = opts
self.col: Optional[_Collection] = None self.col: Optional[_Collection] = None
self.taskman = TaskManager() self.taskman = TaskManager()
self.media_syncer = MediaSyncer(self.taskman, self._on_media_sync_start_stop)
aqt.mw = self aqt.mw = self
self.app = app self.app = app
self.pm = profileManager self.pm = profileManager
@ -830,12 +832,16 @@ title="%s" %s>%s</button>""" % (
# expects a current profile and a loaded collection; reloads # expects a current profile and a loaded collection; reloads
# collection after sync completes # collection after sync completes
def onSync(self): def onSync(self):
if self.media_syncer.is_syncing():
self._show_sync_log()
else:
self.unloadCollection(self._onSync) self.unloadCollection(self._onSync)
def _onSync(self): def _onSync(self):
self._sync() self._sync()
if not self.loadCollection(): if not self.loadCollection():
return return
self._sync_media()
# expects a current profile, but no collection loaded # expects a current profile, but no collection loaded
def maybeAutoSync(self) -> None: def maybeAutoSync(self) -> None:
@ -857,6 +863,31 @@ title="%s" %s>%s</button>""" % (
self.syncer = SyncManager(self, self.pm) self.syncer = SyncManager(self, self.pm)
self.syncer.sync() 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 # Tools
########################################################################## ##########################################################################

View File

@ -1,11 +1,13 @@
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations
import time import time
from concurrent.futures import Future from concurrent.futures import Future
from copy import copy from copy import copy
from dataclasses import dataclass from dataclasses import dataclass
from typing import List, Optional, Union from typing import List, Optional, Union, Callable
import anki import anki
import aqt import aqt
@ -71,11 +73,12 @@ class LogEntryWithTime:
class MediaSyncer: class MediaSyncer:
def __init__(self, taskman: TaskManager): def __init__(self, taskman: TaskManager, on_start_stop: Callable[[], None]):
self._taskman = taskman self._taskman = taskman
self._sync_state: Optional[MediaSyncState] = None self._sync_state: Optional[MediaSyncState] = None
self._log: List[LogEntryWithTime] = [] self._log: List[LogEntryWithTime] = []
self._want_stop = False self._want_stop = False
self._on_start_stop = on_start_stop
hooks.rust_progress_callback.append(self._on_rust_progress) hooks.rust_progress_callback.append(self._on_rust_progress)
def _on_rust_progress(self, proceed: bool, progress: Progress) -> bool: def _on_rust_progress(self, proceed: bool, progress: Progress) -> bool:
@ -111,6 +114,7 @@ class MediaSyncer:
self._log_and_notify(SyncBegun()) self._log_and_notify(SyncBegun())
self._sync_state = MediaSyncState() self._sync_state = MediaSyncState()
self._want_stop = False self._want_stop = False
self._on_start_stop()
if shard is not None: if shard is not None:
shard_str = str(shard) shard_str = str(shard)
@ -134,6 +138,7 @@ class MediaSyncer:
def _on_finished(self, future: Future) -> None: def _on_finished(self, future: Future) -> None:
self._sync_state = None self._sync_state = None
self._on_start_stop()
exc = future.exception() exc = future.exception()
if exc is not None: if exc is not None:
@ -150,10 +155,16 @@ class MediaSyncer:
def abort(self) -> None: def abort(self) -> None:
self._want_stop = True self._want_stop = True
def is_syncing(self) -> bool:
return self._sync_state is not None
class MediaSyncDialog(QDialog): class MediaSyncDialog(QDialog):
def __init__(self, parent: QWidget, syncer: MediaSyncer) -> None: silentlyClose = True
super().__init__(parent)
def __init__(self, mw: aqt.main.AnkiQt, syncer: MediaSyncer) -> None:
super().__init__(mw)
self.mw = mw
self._syncer = syncer self._syncer = syncer
self.form = aqt.forms.synclog.Ui_Dialog() self.form = aqt.forms.synclog.Ui_Dialog()
self.form.setupUi(self) self.form.setupUi(self)
@ -166,6 +177,18 @@ class MediaSyncDialog(QDialog):
self.form.plainTextEdit.setPlainText( self.form.plainTextEdit.setPlainText(
"\n".join(self._entry_to_text(x) for x in syncer.entries()) "\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: def _on_abort(self, *args) -> None:
self.form.plainTextEdit.appendPlainText( self.form.plainTextEdit.appendPlainText(
@ -180,9 +203,9 @@ class MediaSyncDialog(QDialog):
def _entry_to_text(self, entry: LogEntryWithTime): def _entry_to_text(self, entry: LogEntryWithTime):
if isinstance(entry.entry, SyncBegun): if isinstance(entry.entry, SyncBegun):
txt = _("Sync starting...") txt = _("Media sync starting...")
elif isinstance(entry.entry, SyncEnded): elif isinstance(entry.entry, SyncEnded):
txt = _("Sync complete.") txt = _("Media sync complete.")
elif isinstance(entry.entry, SyncAborted): elif isinstance(entry.entry, SyncAborted):
txt = _("Aborted.") txt = _("Aborted.")
elif isinstance(entry.entry, MediaSyncState): elif isinstance(entry.entry, MediaSyncState):

View File

@ -42,7 +42,6 @@ class SyncManager(QObject):
self.pm.collectionPath(), self.pm.collectionPath(),
self.pm.profile["syncKey"], self.pm.profile["syncKey"],
auth=auth, auth=auth,
media=self.pm.profile["syncMedia"],
hostNum=self.pm.profile.get("hostNum"), hostNum=self.pm.profile.get("hostNum"),
) )
t._event.connect(self.onEvent) t._event.connect(self.onEvent)
@ -132,8 +131,6 @@ automatically."""
m = _("Downloading from AnkiWeb...") m = _("Downloading from AnkiWeb...")
elif t == "sanity": elif t == "sanity":
m = _("Checking...") m = _("Checking...")
elif t == "findMedia":
m = _("Checking media...")
elif t == "upgradeRequired": elif t == "upgradeRequired":
showText( showText(
_( _(
@ -154,14 +151,6 @@ Please visit AnkiWeb, upgrade your deck, then try again."""
self._clockOff() self._clockOff()
elif evt == "checkFailed": elif evt == "checkFailed":
self._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": elif evt == "noChanges":
pass pass
elif evt == "fullSync": elif evt == "fullSync":
@ -358,12 +347,11 @@ class SyncThread(QThread):
_event = pyqtSignal(str, str) _event = pyqtSignal(str, str)
progress_event = pyqtSignal(int, int) 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) QThread.__init__(self)
self.path = path self.path = path
self.hkey = hkey self.hkey = hkey
self.auth = auth self.auth = auth
self.media = media
self.hostNum = hostNum self.hostNum = hostNum
self._abort = 0 # 1=flagged, 2=aborting self._abort = 0 # 1=flagged, 2=aborting
@ -475,8 +463,6 @@ class SyncThread(QThread):
self.syncMsg = self.client.syncMsg self.syncMsg = self.client.syncMsg
self.uname = self.client.uname self.uname = self.client.uname
self.hostNum = self.client.hostNum self.hostNum = self.client.hostNum
# then move on to media sync
self._syncMedia()
def _fullSync(self): def _fullSync(self):
# tell the calling thread we need a decision on sync direction, and # 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): if "sync cancelled" in str(e):
return return
raise 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=""): def fireEvent(self, cmd, arg=""):
self._event.emit(cmd, arg) self._event.emit(cmd, arg)

View File

@ -62,9 +62,9 @@ class Toolbar:
["add", _("Add"), _("Shortcut key: %s") % "A"], ["add", _("Add"), _("Shortcut key: %s") % "A"],
["browse", _("Browse"), _("Shortcut key: %s") % "B"], ["browse", _("Browse"), _("Shortcut key: %s") % "B"],
["stats", _("Stats"), _("Shortcut key: %s") % "T"], ["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): def _linkHTML(self, links):
buf = "" buf = ""
@ -78,6 +78,22 @@ class Toolbar:
) )
return buf 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 # 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 { .isMac.nightMode #header {
border-bottom-color: vars.$night-frame-bg; 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;
}