mirror of
https://github.com/ankitects/anki.git
synced 2025-09-25 09:16:38 -04:00
sync updates (syncName, etc)
- remove text box from deck properties - exiting deck properties after enabling syncing forces a sync - instead of the arduous file>sync, enable checkbox, close dialog, file>sync again process users had to go through before, they can just file>sync now and the deck immediately syncs - when downloading a deck, instead of downloading into a new name, prompt if the user wants to overwrite the deck, and cancel if they don't - simplify deckChooser since we only have to deal with downloading now - only display a clobber warning if the deck already existed on the server - disable syncing if user declines to clobber the deck - skip expensive summary generation if downloading or a conflict
This commit is contained in:
parent
3be42fbbfe
commit
a46c9a8b26
4 changed files with 86 additions and 108 deletions
|
@ -38,13 +38,10 @@ class DeckProperties(QDialog):
|
||||||
|
|
||||||
def readData(self):
|
def readData(self):
|
||||||
# syncing
|
# syncing
|
||||||
sn = self.d.syncName
|
if self.d.syncName:
|
||||||
if sn:
|
|
||||||
self.dialog.doSync.setCheckState(Qt.Checked)
|
self.dialog.doSync.setCheckState(Qt.Checked)
|
||||||
self.dialog.syncName.setText(sn)
|
|
||||||
else:
|
else:
|
||||||
self.dialog.doSync.setCheckState(Qt.Unchecked)
|
self.dialog.doSync.setCheckState(Qt.Unchecked)
|
||||||
self.dialog.syncName.setText(self.d.name())
|
|
||||||
# priorities
|
# priorities
|
||||||
self.dialog.highPriority.setText(self.d.highPriority)
|
self.dialog.highPriority.setText(self.d.highPriority)
|
||||||
self.dialog.medPriority.setText(self.d.medPriority)
|
self.dialog.medPriority.setText(self.d.medPriority)
|
||||||
|
@ -160,12 +157,15 @@ class DeckProperties(QDialog):
|
||||||
n = _("Deck Properties")
|
n = _("Deck Properties")
|
||||||
self.d.startProgress()
|
self.d.startProgress()
|
||||||
self.d.setUndoStart(n)
|
self.d.setUndoStart(n)
|
||||||
|
needSync = False
|
||||||
# syncing
|
# syncing
|
||||||
if self.dialog.doSync.checkState() == Qt.Checked:
|
if self.dialog.doSync.checkState() == Qt.Checked:
|
||||||
self.updateField(self.d, 'syncName',
|
old = self.d.syncName
|
||||||
unicode(self.dialog.syncName.text()))
|
self.d.enableSyncing()
|
||||||
|
if self.d.syncName != old:
|
||||||
|
needSync = True
|
||||||
else:
|
else:
|
||||||
self.updateField(self.d, 'syncName', None)
|
self.d.disableSyncing()
|
||||||
# scheduling
|
# scheduling
|
||||||
minmax = ("Min", "Max")
|
minmax = ("Min", "Max")
|
||||||
for type in ("hard", "mid", "easy"):
|
for type in ("hard", "mid", "easy"):
|
||||||
|
@ -231,3 +231,5 @@ class DeckProperties(QDialog):
|
||||||
if self.onFinish:
|
if self.onFinish:
|
||||||
self.onFinish()
|
self.onFinish()
|
||||||
QDialog.reject(self)
|
QDialog.reject(self)
|
||||||
|
if needSync:
|
||||||
|
ankiqt.mw.syncDeck(interactive=-1)
|
||||||
|
|
|
@ -150,6 +150,14 @@ class AnkiQt(QMainWindow):
|
||||||
self.mainWin.noticeButton.setFixedHeight(20)
|
self.mainWin.noticeButton.setFixedHeight(20)
|
||||||
addHook("cardAnswered", self.onCardAnsweredHook)
|
addHook("cardAnswered", self.onCardAnsweredHook)
|
||||||
addHook("undoEnd", self.maybeEnableUndo)
|
addHook("undoEnd", self.maybeEnableUndo)
|
||||||
|
addHook("notify", self.onNotify)
|
||||||
|
|
||||||
|
def onNotify(self, msg):
|
||||||
|
if self.mainThread != QThread.currentThread():
|
||||||
|
# decks may be opened in a sync thread
|
||||||
|
sys.stderr.write(msg + "\n")
|
||||||
|
else:
|
||||||
|
ui.utils.showInfo(msg)
|
||||||
|
|
||||||
def setNotice(self, str=""):
|
def setNotice(self, str=""):
|
||||||
if str:
|
if str:
|
||||||
|
@ -192,7 +200,11 @@ class AnkiQt(QMainWindow):
|
||||||
def getError(self):
|
def getError(self):
|
||||||
p = self.pool
|
p = self.pool
|
||||||
self.pool = ""
|
self.pool = ""
|
||||||
return unicode(p, 'utf8', 'replace')
|
try:
|
||||||
|
return unicode(p, 'utf8', 'replace')
|
||||||
|
except TypeError:
|
||||||
|
# already unicode
|
||||||
|
return p
|
||||||
|
|
||||||
self.errorPipe = ErrorPipe(self)
|
self.errorPipe = ErrorPipe(self)
|
||||||
sys.stderr = self.errorPipe
|
sys.stderr = self.errorPipe
|
||||||
|
@ -212,6 +224,12 @@ class AnkiQt(QMainWindow):
|
||||||
ui.utils.showInfo(
|
ui.utils.showInfo(
|
||||||
_("Couldn't play sound. Please install mplayer."))
|
_("Couldn't play sound. Please install mplayer."))
|
||||||
return
|
return
|
||||||
|
if "ERR-0100" in error:
|
||||||
|
ui.utils.showInfo(error)
|
||||||
|
return
|
||||||
|
if "ERR-0101" in error:
|
||||||
|
ui.utils.showInfo(error)
|
||||||
|
return
|
||||||
stdText = _("""\
|
stdText = _("""\
|
||||||
|
|
||||||
An error occurred. It may have been caused by a harmless bug, <br>
|
An error occurred. It may have been caused by a harmless bug, <br>
|
||||||
|
@ -2099,10 +2117,9 @@ it to your friends.
|
||||||
# Syncing
|
# Syncing
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def syncDeck(self, interactive=True, create=False, onlyMerge=False,
|
def syncDeck(self, interactive=True, onlyMerge=False, reload=True):
|
||||||
reload=True):
|
|
||||||
"Synchronise a deck with the server."
|
"Synchronise a deck with the server."
|
||||||
if not self.inMainWindow() and interactive: return
|
if not self.inMainWindow() and interactive and interactive!=-1: return
|
||||||
self.setNotice()
|
self.setNotice()
|
||||||
# vet input
|
# vet input
|
||||||
if interactive:
|
if interactive:
|
||||||
|
@ -2117,10 +2134,10 @@ it to your friends.
|
||||||
return
|
return
|
||||||
if self.deck and not self.deck.syncName:
|
if self.deck and not self.deck.syncName:
|
||||||
if interactive:
|
if interactive:
|
||||||
self.onDeckProperties()
|
# enable syncing
|
||||||
self.deckProperties.dialog.qtabwidget.setCurrentIndex(1)
|
self.deck.enableSyncing()
|
||||||
self.showToolTip(_("Enable syncing, choose a name, then sync again."))
|
else:
|
||||||
return
|
return
|
||||||
if self.deck is None and getattr(self, 'deckPath', None) is None:
|
if self.deck is None and getattr(self, 'deckPath', None) is None:
|
||||||
# sync all decks
|
# sync all decks
|
||||||
self.loadAfterSync = -1
|
self.loadAfterSync = -1
|
||||||
|
@ -2130,13 +2147,12 @@ it to your friends.
|
||||||
# sync one deck
|
# sync one deck
|
||||||
# hide all deck-associated dialogs
|
# hide all deck-associated dialogs
|
||||||
self.closeAllDeckWindows()
|
self.closeAllDeckWindows()
|
||||||
|
|
||||||
if self.deck:
|
if self.deck:
|
||||||
# save first, so we can rollback on failure
|
# save first, so we can rollback on failure
|
||||||
self.deck.save()
|
self.deck.save()
|
||||||
# store data we need before closing the deck
|
# store data we need before closing the deck
|
||||||
self.deckPath = self.deck.path
|
self.deckPath = self.deck.path
|
||||||
self.syncName = self.deck.syncName or self.deck.name()
|
self.syncName = self.deck.name()
|
||||||
self.lastSync = self.deck.lastSync
|
self.lastSync = self.deck.lastSync
|
||||||
self.deck.close()
|
self.deck.close()
|
||||||
self.deck = None
|
self.deck = None
|
||||||
|
@ -2146,8 +2162,7 @@ it to your friends.
|
||||||
self.state = "nostate"
|
self.state = "nostate"
|
||||||
import gc; gc.collect()
|
import gc; gc.collect()
|
||||||
self.mainWin.welcomeText.setText(u"")
|
self.mainWin.welcomeText.setText(u"")
|
||||||
self.syncThread = ui.sync.Sync(self, u, p, interactive, create,
|
self.syncThread = ui.sync.Sync(self, u, p, interactive, onlyMerge)
|
||||||
onlyMerge)
|
|
||||||
self.connect(self.syncThread, SIGNAL("setStatus"), self.setSyncStatus)
|
self.connect(self.syncThread, SIGNAL("setStatus"), self.setSyncStatus)
|
||||||
self.connect(self.syncThread, SIGNAL("showWarning"), self.showSyncWarning)
|
self.connect(self.syncThread, SIGNAL("showWarning"), self.showSyncWarning)
|
||||||
self.connect(self.syncThread, SIGNAL("noSyncResponse"), self.noSyncResponse)
|
self.connect(self.syncThread, SIGNAL("noSyncResponse"), self.noSyncResponse)
|
||||||
|
@ -2204,7 +2219,7 @@ you want to do?""" % deckName),
|
||||||
diag = ui.utils.askUserDialog(_("""\
|
diag = ui.utils.askUserDialog(_("""\
|
||||||
You are about to upload <b>%s</b>
|
You are about to upload <b>%s</b>
|
||||||
to AnkiOnline. This will overwrite
|
to AnkiOnline. This will overwrite
|
||||||
the online version if one exists.
|
the online copy of this deck.
|
||||||
Are you sure?""" % deckName),
|
Are you sure?""" % deckName),
|
||||||
[_("Upload"),
|
[_("Upload"),
|
||||||
_("Cancel")])
|
_("Cancel")])
|
||||||
|
@ -2231,33 +2246,34 @@ Are you sure?""" % deckName),
|
||||||
if self.loadAfterSync == 2:
|
if self.loadAfterSync == 2:
|
||||||
name = re.sub("[<>]", "", self.syncName)
|
name = re.sub("[<>]", "", self.syncName)
|
||||||
p = os.path.join(self.documentDir, name + ".anki")
|
p = os.path.join(self.documentDir, name + ".anki")
|
||||||
if os.path.exists(p):
|
|
||||||
p = os.path.join(self.documentDir,
|
|
||||||
name + "%d.anki" % time.time())
|
|
||||||
shutil.copy2(self.deckPath, p)
|
shutil.copy2(self.deckPath, p)
|
||||||
self.deckPath = p
|
self.deckPath = p
|
||||||
self.loadDeck(self.deckPath, sync=False)
|
self.loadDeck(self.deckPath, sync=False)
|
||||||
self.deck.syncName = self.syncName
|
if self.loadAfterSync == 2:
|
||||||
self.deck.s.flush()
|
self.deck.enableSyncing()
|
||||||
self.deck.s.commit()
|
|
||||||
else:
|
else:
|
||||||
self.moveToState("noDeck")
|
self.moveToState("noDeck")
|
||||||
self.deckPath = None
|
self.deckPath = None
|
||||||
self.syncFinished = True
|
self.syncFinished = True
|
||||||
|
|
||||||
def selectSyncDeck(self, decks, create=True):
|
def selectSyncDeck(self, decks):
|
||||||
name = ui.sync.DeckChooser(self, decks, create).getName()
|
name = ui.sync.DeckChooser(self, decks).getName()
|
||||||
self.syncName = name
|
self.syncName = name
|
||||||
if name:
|
if name:
|
||||||
# name chosen
|
# name chosen
|
||||||
onlyMerge = self.loadAfterSync == 2
|
p = os.path.join(self.documentDir, name + ".anki")
|
||||||
self.syncDeck(create=True, interactive=False, onlyMerge=onlyMerge)
|
if os.path.exists(p):
|
||||||
else:
|
d = ui.utils.askUserDialog(_("""\
|
||||||
if not create:
|
This deck already exists on your computer. Overwrite the local copy?"""),
|
||||||
self.syncFinished = True
|
["Overwrite", "Cancel"])
|
||||||
self.cleanNewDeck()
|
d.setDefault(1)
|
||||||
|
if d.run() == "Overwrite":
|
||||||
|
self.syncDeck(interactive=False, onlyMerge=True)
|
||||||
else:
|
else:
|
||||||
self.onSyncFinished()
|
self.syncDeck(interactive=False, onlyMerge=True)
|
||||||
|
return
|
||||||
|
self.syncFinished = True
|
||||||
|
self.cleanNewDeck()
|
||||||
|
|
||||||
def cleanNewDeck(self):
|
def cleanNewDeck(self):
|
||||||
"Unload a new deck if an initial sync failed."
|
"Unload a new deck if an initial sync failed."
|
||||||
|
|
|
@ -19,14 +19,12 @@ from anki.hooks import addHook, removeHook
|
||||||
|
|
||||||
class Sync(QThread):
|
class Sync(QThread):
|
||||||
|
|
||||||
def __init__(self, parent, user, pwd, interactive, create,
|
def __init__(self, parent, user, pwd, interactive, onlyMerge):
|
||||||
onlyMerge):
|
|
||||||
QThread.__init__(self)
|
QThread.__init__(self)
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
self.interactive = interactive
|
self.interactive = interactive
|
||||||
self.user = user
|
self.user = user
|
||||||
self.pwd = pwd
|
self.pwd = pwd
|
||||||
self.create = create
|
|
||||||
self.ok = True
|
self.ok = True
|
||||||
self.onlyMerge = onlyMerge
|
self.onlyMerge = onlyMerge
|
||||||
self.proxy = None
|
self.proxy = None
|
||||||
|
@ -122,6 +120,7 @@ sync was aborted. Please report this error.""")
|
||||||
c.close()
|
c.close()
|
||||||
if not syncName:
|
if not syncName:
|
||||||
return
|
return
|
||||||
|
syncName = os.path.splitext(os.path.basename(deck))[0]
|
||||||
path = deck
|
path = deck
|
||||||
else:
|
else:
|
||||||
syncName = self.parent.syncName
|
syncName = self.parent.syncName
|
||||||
|
@ -147,19 +146,21 @@ sync was aborted. Please report this error.""")
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
# exists on server?
|
# exists on server?
|
||||||
|
deckCreated = False
|
||||||
if not proxy.hasDeck(syncName):
|
if not proxy.hasDeck(syncName):
|
||||||
|
# multi-mode?
|
||||||
if deck:
|
if deck:
|
||||||
return
|
return
|
||||||
if self.create:
|
if self.onlyMerge:
|
||||||
try:
|
|
||||||
proxy.createDeck(syncName)
|
|
||||||
except SyncError, e:
|
|
||||||
return self.error(e)
|
|
||||||
else:
|
|
||||||
keys = [k for (k,v) in proxy.decks.items() if v[1] != -1]
|
keys = [k for (k,v) in proxy.decks.items() if v[1] != -1]
|
||||||
self.emit(SIGNAL("noMatchingDeck"), keys, not self.onlyMerge)
|
self.emit(SIGNAL("noMatchingDeck"), keys)
|
||||||
self.setStatus("")
|
self.setStatus("")
|
||||||
return
|
return
|
||||||
|
try:
|
||||||
|
proxy.createDeck(syncName)
|
||||||
|
deckCreated = True
|
||||||
|
except SyncError, e:
|
||||||
|
return self.error(e)
|
||||||
# check conflicts
|
# check conflicts
|
||||||
proxy.deckName = syncName
|
proxy.deckName = syncName
|
||||||
remoteMod = proxy.modified()
|
remoteMod = proxy.modified()
|
||||||
|
@ -188,25 +189,36 @@ sync was aborted. Please report this error.""")
|
||||||
if client.prepareSync():
|
if client.prepareSync():
|
||||||
changes = True
|
changes = True
|
||||||
# summary
|
# summary
|
||||||
self.setStatus(_("Fetching summary from server..."), 0)
|
if not self.conflictResolution and not self.onlyMerge:
|
||||||
sums = client.summaries()
|
self.setStatus(_("Fetching summary from server..."), 0)
|
||||||
if self.conflictResolution or client.needFullSync(sums):
|
sums = client.summaries()
|
||||||
|
if (self.conflictResolution or
|
||||||
|
self.onlyMerge or client.needFullSync(sums)):
|
||||||
self.setStatus(_("Preparing full sync..."), 0)
|
self.setStatus(_("Preparing full sync..."), 0)
|
||||||
if self.conflictResolution == "keepLocal":
|
if self.conflictResolution == "keepLocal":
|
||||||
client.remoteTime = 0
|
client.remoteTime = 0
|
||||||
elif self.conflictResolution == "keepRemote":
|
elif self.conflictResolution == "keepRemote" or self.onlyMerge:
|
||||||
client.localTime = 0
|
client.localTime = 0
|
||||||
lastSync = self.deck.lastSync
|
lastSync = self.deck.lastSync
|
||||||
ret = client.prepareFullSync()
|
ret = client.prepareFullSync()
|
||||||
if ret[0] == "fromLocal":
|
if ret[0] == "fromLocal":
|
||||||
if not self.conflictResolution:
|
if not self.conflictResolution:
|
||||||
if lastSync <= 0:
|
if lastSync <= 0 and not deckCreated:
|
||||||
self.clobberChoice = None
|
self.clobberChoice = None
|
||||||
self.emit(SIGNAL("syncClobber"), syncName)
|
self.emit(SIGNAL("syncClobber"), syncName)
|
||||||
while not self.clobberChoice:
|
while not self.clobberChoice:
|
||||||
time.sleep(0.2)
|
time.sleep(0.2)
|
||||||
if self.clobberChoice == "cancel":
|
if self.clobberChoice == "cancel":
|
||||||
|
# deck has already been closed in
|
||||||
|
# prepareFullSync(), so clean up
|
||||||
self.deck.close()
|
self.deck.close()
|
||||||
|
# disable syncing on this deck
|
||||||
|
c = sqlite.connect(sqlpath)
|
||||||
|
c.execute(
|
||||||
|
"update decks set syncName = null, "
|
||||||
|
"lastSync = 0")
|
||||||
|
c.commit()
|
||||||
|
c.close()
|
||||||
if not deck:
|
if not deck:
|
||||||
# alert we're finished early
|
# alert we're finished early
|
||||||
self.emit(SIGNAL("syncFinished"))
|
self.emit(SIGNAL("syncFinished"))
|
||||||
|
@ -260,32 +272,22 @@ sync was aborted. Please report this error.""")
|
||||||
if not deck:
|
if not deck:
|
||||||
self.error(e)
|
self.error(e)
|
||||||
|
|
||||||
# Choosing a deck to sync to
|
# Downloading personal decks
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
class DeckChooser(QDialog):
|
class DeckChooser(QDialog):
|
||||||
|
|
||||||
def __init__(self, parent, decks, create):
|
def __init__(self, parent, decks):
|
||||||
QDialog.__init__(self, parent, Qt.Window)
|
QDialog.__init__(self, parent, Qt.Window)
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
self.decks = decks
|
self.decks = decks
|
||||||
self.dialog = ankiqt.forms.syncdeck.Ui_DeckChooser()
|
self.dialog = ankiqt.forms.syncdeck.Ui_DeckChooser()
|
||||||
self.dialog.setupUi(self)
|
self.dialog.setupUi(self)
|
||||||
self.create = create
|
self.dialog.topLabel.setText(_("<h1>Dowload Personal Deck</h1>"))
|
||||||
if self.create:
|
|
||||||
self.dialog.topLabel.setText(_("<h1>Synchronize</h1>"))
|
|
||||||
else:
|
|
||||||
self.dialog.topLabel.setText(_("<h1>Open Online Deck</h1>"))
|
|
||||||
if self.create:
|
|
||||||
self.dialog.decks.addItem(QListWidgetItem(
|
|
||||||
_("Create '%s' on server") % self.parent.syncName))
|
|
||||||
self.decks.sort()
|
self.decks.sort()
|
||||||
for name in decks:
|
for name in decks:
|
||||||
name = os.path.splitext(name)[0]
|
name = os.path.splitext(name)[0]
|
||||||
if self.create:
|
msg = name
|
||||||
msg = _("Overwrite '%s' on server") % name
|
|
||||||
else:
|
|
||||||
msg = name
|
|
||||||
item = QListWidgetItem(msg)
|
item = QListWidgetItem(msg)
|
||||||
self.dialog.decks.addItem(item)
|
self.dialog.decks.addItem(item)
|
||||||
self.dialog.decks.setCurrentRow(0)
|
self.dialog.decks.setCurrentRow(0)
|
||||||
|
@ -300,12 +302,5 @@ class DeckChooser(QDialog):
|
||||||
|
|
||||||
def accept(self):
|
def accept(self):
|
||||||
idx = self.dialog.decks.currentRow()
|
idx = self.dialog.decks.currentRow()
|
||||||
if self.create:
|
self.name = self.decks[self.dialog.decks.currentRow()]
|
||||||
offset = 1
|
|
||||||
else:
|
|
||||||
offset = 0
|
|
||||||
if idx == 0 and self.create:
|
|
||||||
self.name = self.parent.syncName
|
|
||||||
else:
|
|
||||||
self.name = self.decks[self.dialog.decks.currentRow() - offset]
|
|
||||||
self.close()
|
self.close()
|
||||||
|
|
|
@ -195,25 +195,7 @@
|
||||||
</spacer>
|
</spacer>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="0">
|
<item row="2" column="0">
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
<layout class="QHBoxLayout" name="horizontalLayout_2"/>
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="label_18">
|
|
||||||
<property name="text">
|
|
||||||
<string>Name on server: </string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QLineEdit" name="syncName">
|
|
||||||
<property name="enabled">
|
|
||||||
<bool>false</bool>
|
|
||||||
</property>
|
|
||||||
<property name="whatsThis">
|
|
||||||
<string>option</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
|
@ -561,7 +543,6 @@
|
||||||
<tabstop>medPriority</tabstop>
|
<tabstop>medPriority</tabstop>
|
||||||
<tabstop>lowPriority</tabstop>
|
<tabstop>lowPriority</tabstop>
|
||||||
<tabstop>doSync</tabstop>
|
<tabstop>doSync</tabstop>
|
||||||
<tabstop>syncName</tabstop>
|
|
||||||
<tabstop>delay0</tabstop>
|
<tabstop>delay0</tabstop>
|
||||||
<tabstop>delay1</tabstop>
|
<tabstop>delay1</tabstop>
|
||||||
<tabstop>delay2</tabstop>
|
<tabstop>delay2</tabstop>
|
||||||
|
@ -612,21 +593,5 @@
|
||||||
</hint>
|
</hint>
|
||||||
</hints>
|
</hints>
|
||||||
</connection>
|
</connection>
|
||||||
<connection>
|
|
||||||
<sender>doSync</sender>
|
|
||||||
<signal>toggled(bool)</signal>
|
|
||||||
<receiver>syncName</receiver>
|
|
||||||
<slot>setEnabled(bool)</slot>
|
|
||||||
<hints>
|
|
||||||
<hint type="sourcelabel">
|
|
||||||
<x>58</x>
|
|
||||||
<y>445</y>
|
|
||||||
</hint>
|
|
||||||
<hint type="destinationlabel">
|
|
||||||
<x>327</x>
|
|
||||||
<y>477</y>
|
|
||||||
</hint>
|
|
||||||
</hints>
|
|
||||||
</connection>
|
|
||||||
</connections>
|
</connections>
|
||||||
</ui>
|
</ui>
|
||||||
|
|
Loading…
Reference in a new issue