mirror of
https://github.com/ankitects/anki.git
synced 2025-11-10 06:37:12 -05:00
Merge branch 'master' of https://github.com/dae/anki
This commit is contained in:
commit
2339f4bafc
17 changed files with 137 additions and 56 deletions
|
|
@ -2,12 +2,16 @@
|
|||
# Copyright: Damien Elmes <anki@ichi2.net>
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import os, time
|
||||
import os
|
||||
import time
|
||||
|
||||
try:
|
||||
from pysqlite2 import dbapi2 as sqlite
|
||||
except ImportError:
|
||||
from sqlite3 import dbapi2 as sqlite
|
||||
|
||||
Error = sqlite.Error
|
||||
|
||||
class DB(object):
|
||||
def __init__(self, path, text=None, timeout=0):
|
||||
encpath = path
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@
|
|||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import re
|
||||
import sre_constants
|
||||
|
||||
from anki.utils import ids2str, splitFields, joinFields, intTime, fieldChecksum, stripHTMLMedia
|
||||
from anki.consts import *
|
||||
from anki.hooks import *
|
||||
import sre_constants
|
||||
|
||||
|
||||
# Find
|
||||
##########################################################################
|
||||
|
|
@ -257,7 +259,9 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """+preds
|
|||
return "queue in (1, 3)"
|
||||
return "type = %d" % n
|
||||
elif val == "suspended":
|
||||
return "c.queue in (-1, -2)"
|
||||
return "c.queue = -1"
|
||||
elif val == "buried":
|
||||
return "c.queue = -2"
|
||||
elif val == "due":
|
||||
return """
|
||||
(c.queue in (2,3) and c.due <= %d) or
|
||||
|
|
|
|||
|
|
@ -45,7 +45,10 @@ class MediaManager(object):
|
|||
except OSError:
|
||||
# cwd doesn't exist
|
||||
self._oldcwd = None
|
||||
os.chdir(self._dir)
|
||||
try:
|
||||
os.chdir(self._dir)
|
||||
except OSError:
|
||||
raise Exception("invalidTempFolder")
|
||||
# change database
|
||||
self.connect()
|
||||
|
||||
|
|
|
|||
|
|
@ -945,7 +945,7 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
|
|||
def _fillDyn(self, deck):
|
||||
search, limit, order = deck['terms'][0]
|
||||
orderlimit = self._dynOrder(order, limit)
|
||||
search += " -is:suspended -deck:filtered"
|
||||
search += " -is:suspended -is:buried -deck:filtered"
|
||||
try:
|
||||
ids = self.col.findCards(search, order=orderlimit)
|
||||
except:
|
||||
|
|
@ -1266,16 +1266,19 @@ To study outside of the normal schedule, click the Custom Study button below."""
|
|||
"where queue = -1 and id in "+ ids2str(ids),
|
||||
intTime(), self.col.usn())
|
||||
|
||||
def buryNote(self, nid):
|
||||
"Bury all cards for note until next session."
|
||||
cids = self.col.db.list(
|
||||
"select id from cards where nid = ? and queue >= 0", nid)
|
||||
def buryCards(self, cids):
|
||||
self.col.log(cids)
|
||||
self.removeLrn(cids)
|
||||
self.col.db.execute("""
|
||||
update cards set queue=-2,mod=?,usn=? where id in """+ids2str(cids),
|
||||
intTime(), self.col.usn())
|
||||
|
||||
def buryNote(self, nid):
|
||||
"Bury all cards for note until next session."
|
||||
cids = self.col.db.list(
|
||||
"select id from cards where nid = ? and queue >= 0", nid)
|
||||
self.buryCards(cids)
|
||||
|
||||
# Sibling spacing
|
||||
##########################################################################
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import anki
|
|||
HTTP_TIMEOUT = 90
|
||||
HTTP_PROXY = None
|
||||
|
||||
# badly named; means no retries, and doesn't affect ssl connections
|
||||
# badly named; means no retries
|
||||
httplib2.RETRIES = 1
|
||||
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
# Copyright: Damien Elmes <anki@ichi2.net>
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
import getpass
|
||||
import os
|
||||
import sys
|
||||
import optparse
|
||||
import tempfile
|
||||
import __builtin__
|
||||
import locale
|
||||
import gettext
|
||||
|
||||
import os, sys, optparse, atexit, tempfile, __builtin__
|
||||
from aqt.qt import *
|
||||
import locale, gettext
|
||||
import anki.lang
|
||||
from anki.consts import HELP_SITE
|
||||
from anki.lang import langDir
|
||||
|
|
@ -157,6 +162,7 @@ class AnkiApp(QApplication):
|
|||
sys.stderr.write(sock.errorString())
|
||||
return
|
||||
buf = sock.readAll()
|
||||
buf = unicode(buf, sys.getfilesystemencoding(), "ignore")
|
||||
self.emit(SIGNAL("appMsg"), buf)
|
||||
sock.disconnectFromServer()
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ Montague, Michael Penkov, Michal Čadil, Morteza Salehi, Nathanael Law, Nick Coo
|
|||
Laxström, Nguyễn Hào Khôi, Norbert Nagold, Ole Guldberg,
|
||||
Pcsl88, Petr Michalec, Piotr Kubowicz, Richard Colley, Roland Sieker,
|
||||
Samson Melamed, Stefaan De Pooter, Silja Ijas, Snezana Lukic, Susanna Björverud, Sylvain Durand,
|
||||
Tacutu, Timm Preetz, Timo Paulssen, Ursus, Victor Suba, Xtru %s 黃文龍
|
||||
Tacutu, Timm Preetz, Timo Paulssen, Ursus, Victor Suba, Volodymyr Goncharenko, Xtru %s 黃文龍
|
||||
"""% _("<!--about diag--> and")}
|
||||
abouttext += '<p>' + _("""\
|
||||
The icons were obtained from various sources; please see the Anki source
|
||||
|
|
|
|||
|
|
@ -2,18 +2,20 @@
|
|||
# Copyright: Damien Elmes <anki@ichi2.net>
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
from aqt.qt import *
|
||||
import re
|
||||
|
||||
from aqt.qt import *
|
||||
from anki.consts import *
|
||||
import aqt
|
||||
from anki.sound import playFromText, clearAudioQueue
|
||||
from aqt.utils import saveGeom, restoreGeom, getBase, mungeQA,\
|
||||
showInfo, askUser, getOnlyText, \
|
||||
showWarning, openHelp, openLink
|
||||
showWarning, openHelp
|
||||
from anki.utils import isMac, isWin, joinFields
|
||||
from aqt.webview import AnkiWebView
|
||||
import anki.js
|
||||
|
||||
|
||||
class CardLayout(QDialog):
|
||||
|
||||
def __init__(self, mw, note, ord=0, parent=None, addMode=False):
|
||||
|
|
@ -135,7 +137,7 @@ class CardLayout(QDialog):
|
|||
return showInfo(_("At least one card type is required."))
|
||||
cards = self.mm.tmplUseCount(self.model, idx)
|
||||
cards = ngettext("%d card", "%d cards", cards) % cards
|
||||
msg = _("Delete the '%(a)s' card type, and its %(b)s?" %
|
||||
msg = (_("Delete the '%(a)s' card type, and its %(b)s?") %
|
||||
dict(a=self.model['tmpls'][idx]['name'], b=cards))
|
||||
if not askUser(msg):
|
||||
return
|
||||
|
|
|
|||
|
|
@ -779,7 +779,7 @@ to a cloze type first, via Edit>Change Note Type."""))
|
|||
except Exception, e:
|
||||
showWarning(_(
|
||||
"Couldn't record audio. Have you installed lame and sox?") +
|
||||
"\n\n" + unicode(e))
|
||||
"\n\n" + repr(str(e)))
|
||||
return
|
||||
self.addMedia(file)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
# Copyright: Damien Elmes <anki@ichi2.net>
|
||||
# -*- coding: utf-8 -*-
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
from aqt.qt import *
|
||||
import sys
|
||||
import cgi
|
||||
|
||||
from anki.lang import _
|
||||
from aqt.qt import *
|
||||
from aqt.utils import showText, showWarning
|
||||
|
||||
class ErrorHandler(QObject):
|
||||
|
|
@ -43,6 +44,12 @@ class ErrorHandler(QObject):
|
|||
self.timer.setSingleShot(True)
|
||||
self.timer.start()
|
||||
|
||||
def tempFolderMsg(self):
|
||||
return _("""\
|
||||
The permissions on your system's temporary folder are incorrect, and Anki is \
|
||||
not able to correct them automatically. Please search for 'temp folder' in the \
|
||||
Anki manual for more information.""")
|
||||
|
||||
def onTimeout(self):
|
||||
error = cgi.escape(self.pool)
|
||||
self.pool = ""
|
||||
|
|
@ -56,6 +63,8 @@ class ErrorHandler(QObject):
|
|||
if "no default output" in error:
|
||||
return showWarning(_("Please connect a microphone, and ensure "
|
||||
"other programs are not using the audio device."))
|
||||
if "invalidTempFolder" in error:
|
||||
return showWarning(self.tempFolderMsg())
|
||||
stdText = _("""\
|
||||
An error occurred. It may have been caused by a harmless bug, <br>
|
||||
or your deck may have a problem.
|
||||
|
|
|
|||
|
|
@ -1,13 +1,22 @@
|
|||
# coding=utf-8
|
||||
# Copyright: Damien Elmes <anki@ichi2.net>
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import os, re, traceback, zipfile, json
|
||||
import os
|
||||
import re
|
||||
import traceback
|
||||
import zipfile
|
||||
import json
|
||||
|
||||
from aqt.qt import *
|
||||
import anki.importing as importing
|
||||
from aqt.utils import getOnlyText, getFile, showText, showWarning, openHelp,\
|
||||
askUser, tooltip
|
||||
from anki.hooks import addHook, remHook
|
||||
import aqt.forms, aqt.modelchooser, aqt.deckchooser
|
||||
import aqt.forms
|
||||
import aqt.modelchooser
|
||||
import aqt.deckchooser
|
||||
|
||||
|
||||
class ChangeMap(QDialog):
|
||||
def __init__(self, mw, model, current):
|
||||
|
|
@ -155,7 +164,7 @@ you can enter it here. Use \\t to represent tab."""),
|
|||
return
|
||||
except Exception, e:
|
||||
msg = _("Import failed.\n")
|
||||
err = unicode(e)
|
||||
err = repr(str(e))
|
||||
if "1-character string" in err:
|
||||
msg += err
|
||||
else:
|
||||
|
|
@ -283,7 +292,7 @@ def importFile(mw, file):
|
|||
showUnicodeWarning()
|
||||
return
|
||||
except Exception, e:
|
||||
msg = unicode(e)
|
||||
msg = repr(str(e))
|
||||
if msg == "unknownFormat":
|
||||
if file.endswith(".anki2"):
|
||||
showWarning(_("""\
|
||||
|
|
@ -322,11 +331,12 @@ failed. Please try again, and if the problem persists, please try again \
|
|||
with a different browser.""")
|
||||
showWarning(msg)
|
||||
except Exception, e:
|
||||
if "invalidFile" in unicode(e):
|
||||
err = repr(str(e))
|
||||
if "invalidFile" in err:
|
||||
msg = _("""\
|
||||
Invalid file. Please restore from backup.""")
|
||||
showWarning(msg)
|
||||
elif "readonly" in unicode(e):
|
||||
elif "readonly" in err:
|
||||
showWarning(_("""\
|
||||
Unable to import from a read-only file."""))
|
||||
else:
|
||||
|
|
|
|||
57
aqt/main.py
57
aqt/main.py
|
|
@ -24,7 +24,7 @@ import aqt.stats
|
|||
from aqt.utils import restoreGeom, showInfo, showWarning,\
|
||||
restoreState, getOnlyText, askUser, applyStyles, showText, tooltip, \
|
||||
openHelp, openLink, checkInvalidFilename
|
||||
|
||||
import anki.db
|
||||
|
||||
class AnkiQt(QMainWindow):
|
||||
def __init__(self, app, profileManager, args):
|
||||
|
|
@ -64,7 +64,7 @@ class AnkiQt(QMainWindow):
|
|||
"syncing and add-on loading."))
|
||||
# were we given a file to import?
|
||||
if args and args[0]:
|
||||
self.onAppMsg(unicode(args[0], "utf8", "ignore"))
|
||||
self.onAppMsg(unicode(args[0], sys.getfilesystemencoding(), "ignore"))
|
||||
# Load profile in a timer so we can let the window finish init and not
|
||||
# close on profile load error.
|
||||
self.progress.timer(10, self.setupProfile, False)
|
||||
|
|
@ -229,7 +229,12 @@ Are you sure?""")):
|
|||
self.activateWindow()
|
||||
self.raise_()
|
||||
# maybe sync (will load DB)
|
||||
self.onSync(auto=True)
|
||||
if self.pendingImport and os.path.basename(
|
||||
self.pendingImport).startswith("backup-"):
|
||||
# skip sync when importing a backup
|
||||
self.loadCollection()
|
||||
else:
|
||||
self.onSync(auto=True)
|
||||
# import pending?
|
||||
if self.pendingImport:
|
||||
if self.pm.profile['key']:
|
||||
|
|
@ -265,13 +270,22 @@ To import into a password protected profile, please open the profile before atte
|
|||
self.hideSchemaMsg = True
|
||||
try:
|
||||
self.col = Collection(self.pm.collectionPath())
|
||||
except:
|
||||
except anki.db.Error:
|
||||
# move back to profile manager
|
||||
showWarning("""\
|
||||
Your collection is corrupt. Please see the manual for \
|
||||
how to restore from a backup.""")
|
||||
self.unloadProfile()
|
||||
raise
|
||||
except Exception, e:
|
||||
# the custom exception handler won't catch this if we immediately
|
||||
# unload, so we have to manually handle it
|
||||
if "invalidTempFolder" in repr(str(e)):
|
||||
showWarning(self.errorHandler.tempFolderMsg())
|
||||
self.unloadProfile()
|
||||
return
|
||||
self.unloadProfile()
|
||||
raise
|
||||
self.hideSchemaMsg = False
|
||||
self.progress.setupDB(self.col.db)
|
||||
self.maybeEnableUndo()
|
||||
|
|
@ -290,7 +304,10 @@ how to restore from a backup.""")
|
|||
return
|
||||
self.maybeOptimize()
|
||||
self.progress.start(immediate=True)
|
||||
corrupt = self.col.db.scalar("pragma integrity_check") != "ok"
|
||||
if os.getenv("ANKIDEV", 0):
|
||||
corrupt = False
|
||||
else:
|
||||
corrupt = self.col.db.scalar("pragma integrity_check") != "ok"
|
||||
if corrupt:
|
||||
showWarning(_("Your collection file appears to be corrupt. \
|
||||
This can happen when the file is copied or moved while Anki is open, or \
|
||||
|
|
@ -309,7 +326,7 @@ the manual for information on how to restore from an automatic backup."))
|
|||
|
||||
def backup(self):
|
||||
nbacks = self.pm.profile['numBackups']
|
||||
if not nbacks:
|
||||
if not nbacks or os.getenv("ANKIDEV", 0):
|
||||
return
|
||||
dir = self.pm.backupFolder()
|
||||
path = self.pm.collectionPath()
|
||||
|
|
@ -808,16 +825,24 @@ title="%s">%s</button>''' % (
|
|||
def newMsg(self, data):
|
||||
aqt.update.showMessages(self, data)
|
||||
|
||||
def clockIsOff(self):
|
||||
showWarning("""\
|
||||
def clockIsOff(self, diff):
|
||||
diffText = ngettext("%s second", "%s seconds", diff)
|
||||
warn = _("""\
|
||||
In order to ensure your collection works correctly when moved between \
|
||||
devices, Anki requires the system clock to be set correctly. Your system \
|
||||
clock appears to be wrong by more than 5 minutes.
|
||||
devices, Anki requires your computer's internal clock to be set correctly. \
|
||||
The internal clock can be wrong even if your system is showing the correct \
|
||||
local time.
|
||||
|
||||
This can be because the \
|
||||
clock is slow or fast, because the date is set incorrectly, or because \
|
||||
the timezone or daylight savings information is incorrect. Please correct \
|
||||
the problem and restart Anki.""")
|
||||
Please go to the time settings on your computer and check the following:
|
||||
|
||||
- AM/PM
|
||||
- Clock drift
|
||||
- Day, month and year
|
||||
- Timezone
|
||||
- Daylight savings
|
||||
|
||||
Difference to correct time: %s.""") % diffText
|
||||
showWarning(warn)
|
||||
self.app.closeAllWindows()
|
||||
|
||||
# Count refreshing
|
||||
|
|
@ -1106,10 +1131,6 @@ will be lost. Continue?"""))
|
|||
self.connect(self.app, SIGNAL("appMsg"), self.onAppMsg)
|
||||
|
||||
def onAppMsg(self, buf):
|
||||
if not isinstance(buf, unicode):
|
||||
# even though we're sending this as unicode up above,
|
||||
# a bug report still came in that we were receiving a qbytearray
|
||||
buf = unicode(buf, "utf8", "ignore")
|
||||
if self.state == "startup":
|
||||
# try again in a second
|
||||
return self.progress.timer(1000, lambda: self.onAppMsg(buf), False)
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@
|
|||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
from __future__ import division
|
||||
import difflib, re, cgi
|
||||
import difflib
|
||||
import re
|
||||
import cgi
|
||||
import unicodedata as ucd
|
||||
import HTMLParser
|
||||
|
||||
from anki.lang import _, ngettext
|
||||
from aqt.qt import *
|
||||
from anki.utils import stripHTML, isMac, json
|
||||
|
|
@ -15,6 +18,7 @@ from aqt.utils import mungeQA, getBase, openLink, tooltip, askUserDialog
|
|||
from aqt.sound import getAudio
|
||||
import aqt
|
||||
|
||||
|
||||
class Reviewer(object):
|
||||
"Manage reviews. Maintains a separate state."
|
||||
|
||||
|
|
@ -285,8 +289,10 @@ The front of this card is empty. Please run Tools>Empty Cards.""")
|
|||
self.replayAudio()
|
||||
elif key == "*":
|
||||
self.onMark()
|
||||
elif key == "-":
|
||||
elif key == "=":
|
||||
self.onBuryNote()
|
||||
elif key == "-":
|
||||
self.onBuryCard()
|
||||
elif key == "!":
|
||||
self.onSuspend()
|
||||
elif key == "@":
|
||||
|
|
@ -678,7 +684,8 @@ function showAnswer(txt) {
|
|||
def showContextMenu(self):
|
||||
opts = [
|
||||
[_("Mark Note"), "*", self.onMark],
|
||||
[_("Bury Note"), "-", self.onBuryNote],
|
||||
[_("Bury Card"), "-", self.onBuryCard],
|
||||
[_("Bury Note"), "=", self.onBuryNote],
|
||||
[_("Suspend Card"), "@", self.onSuspendCard],
|
||||
[_("Suspend Note"), "!", self.onSuspend],
|
||||
[_("Delete Note"), "Delete", self.onDelete],
|
||||
|
|
@ -740,6 +747,12 @@ function showAnswer(txt) {
|
|||
"Note and its %d cards deleted.",
|
||||
cnt) % cnt)
|
||||
|
||||
def onBuryCard(self):
|
||||
self.mw.checkpoint(_("Bury"))
|
||||
self.mw.col.sched.buryCards([self.card.id])
|
||||
self.mw.reset()
|
||||
tooltip(_("Card buried."))
|
||||
|
||||
def onBuryNote(self):
|
||||
self.mw.checkpoint(_("Bury"))
|
||||
self.mw.col.sched.buryNote(self.card.nid)
|
||||
|
|
|
|||
|
|
@ -275,6 +275,10 @@ class SyncThread(QThread):
|
|||
self.media = media
|
||||
|
||||
def run(self):
|
||||
# init this first so an early crash doesn't cause an error
|
||||
# in the main thread
|
||||
self.syncMsg = ""
|
||||
self.uname = ""
|
||||
try:
|
||||
self.col = Collection(self.path)
|
||||
except:
|
||||
|
|
@ -282,8 +286,6 @@ class SyncThread(QThread):
|
|||
return
|
||||
self.server = RemoteServer(self.hkey)
|
||||
self.client = Syncer(self.col, self.server)
|
||||
self.syncMsg = ""
|
||||
self.uname = ""
|
||||
self.sentTotal = 0
|
||||
self.recvTotal = 0
|
||||
# throttle updates; qt doesn't handle lots of posted events well
|
||||
|
|
@ -447,7 +449,7 @@ httplib.HTTPConnection.send = _incrementalSend
|
|||
|
||||
# receiving in httplib2
|
||||
def _conn_request(self, conn, request_uri, method, body, headers):
|
||||
for i in range(2):
|
||||
for i in range(httplib2.RETRIES):
|
||||
try:
|
||||
if conn.sock is None:
|
||||
conn.connect()
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
# Copyright: Damien Elmes <anki@ichi2.net>
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import urllib
|
||||
import urllib2
|
||||
import time
|
||||
|
||||
from aqt.qt import *
|
||||
import urllib, urllib2, time
|
||||
import aqt
|
||||
import platform
|
||||
from aqt.utils import openLink
|
||||
from anki.utils import json, isWin, isMac, platDesc
|
||||
from anki.utils import json, platDesc
|
||||
from aqt.utils import showText
|
||||
|
||||
|
||||
class LatestVersionFinder(QThread):
|
||||
|
||||
def __init__(self, main):
|
||||
|
|
@ -45,7 +48,7 @@ class LatestVersionFinder(QThread):
|
|||
self.emit(SIGNAL("newVerAvail"), resp['ver'])
|
||||
diff = resp['time'] - time.time()
|
||||
if abs(diff) > 300:
|
||||
self.emit(SIGNAL("clockIsOff"))
|
||||
self.emit(SIGNAL("clockIsOff"), diff)
|
||||
|
||||
def askAndUpdate(mw, ver):
|
||||
baseStr = (
|
||||
|
|
|
|||
|
|
@ -259,16 +259,16 @@ class UpgradeThread(QThread):
|
|||
try:
|
||||
self.maybeCopyFromCustomFolder(path)
|
||||
except Exception, e:
|
||||
imp.log.append(unicode(e))
|
||||
imp.log.append(repr(str(e)))
|
||||
# then run the import
|
||||
try:
|
||||
imp.run()
|
||||
except Exception, e:
|
||||
if unicode(e) == "invalidFile":
|
||||
if repr(str(e)) == "invalidFile":
|
||||
# already logged
|
||||
pass
|
||||
else:
|
||||
imp.log.append(unicode(e))
|
||||
imp.log.append(repr(str(e)))
|
||||
self.col.save()
|
||||
return imp.log
|
||||
|
||||
|
|
|
|||
1
thirdparty/send2trash/plat_win.py
vendored
1
thirdparty/send2trash/plat_win.py
vendored
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
from ctypes import windll, Structure, byref, c_uint
|
||||
from ctypes.wintypes import HWND, UINT, LPCWSTR, BOOL
|
||||
import os
|
||||
import os.path as op
|
||||
|
||||
shell32 = windll.shell32
|
||||
|
|
|
|||
Loading…
Reference in a new issue