show spinner when media sync active, click to reveal dialog
This commit is contained in:
parent
ea4de9a6de
commit
cb0ce4146f
@ -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):
|
||||
|
@ -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
|
||||
##########################################################################
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
######################################################################
|
||||
|
||||
|
1
qt/aqt_data/web/imgs/refresh.svg
Normal file
1
qt/aqt_data/web/imgs/refresh.svg
Normal 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 |
@ -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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user