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 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):
|
||||||
|
@ -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
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
|
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 {
|
.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;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user