From 0a2069742c648025684cf73e88587c0fa6c8d430 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Thu, 24 Jan 2019 08:44:28 +0100 Subject: [PATCH] Update README --- README.md | 7 +- __init__.py | 1303 --------------------------------------------------- 2 files changed, 4 insertions(+), 1306 deletions(-) delete mode 100644 __init__.py diff --git a/README.md b/README.md index d1c962a..2510435 100644 --- a/README.md +++ b/README.md @@ -1342,10 +1342,11 @@ guarantee that your application continues to function properly in the future. * **guiAddCards** - Invokes the *Add Cards* dialog and presets the note using the given deck and model, with the provided field values - and tags. Invoking it multiple times will open multiple windows. + Invokes the *Add Cards* dialog, presets the note using the given deck and model, with the provided field values and tags. + Invoking it multiple times closes the old window and _reopen the window_ with the new provided values. The `closeAfterAdding` member inside `options` group can be set to true to create a dialog that closes upon adding the note. + Invoking the action mutliple times with this option will create _multiple windows_. *Sample request*: ```json @@ -1364,7 +1365,7 @@ guarantee that your application continues to function properly in the future. "closeAfterAdding": true }, "tags": [ - "yomichan" + "countries" ] } } diff --git a/__init__.py b/__init__.py deleted file mode 100644 index c31e4b0..0000000 --- a/__init__.py +++ /dev/null @@ -1,1303 +0,0 @@ -# Copyright (C) 2016 Alex Yatskov -# Author: Alex Yatskov -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - - -import anki -import aqt -import base64 -import hashlib -import inspect -import json -import os -import os.path -import re -import select -import socket -import sys - -from operator import itemgetter -from time import time -from unicodedata import normalize -from random import choice -from string import ascii_letters - - -# -# Constants -# - -API_VERSION = 6 -API_LOG_PATH = None -NET_ADDRESS = os.getenv('ANKICONNECT_BIND_ADDRESS', '127.0.0.1') -NET_BACKLOG = 5 -NET_PORT = 8765 -TICK_INTERVAL = 25 -URL_TIMEOUT = 10 -URL_UPGRADE = 'https://raw.githubusercontent.com/FooSoft/anki-connect/master/AnkiConnect.py' - -ANKI21 = anki.version.startswith('2.1') - -# -# Helpers -# - -if sys.version_info[0] < 3: - import urllib2 - def download(url): - contents = None - resp = urllib2.urlopen(url, timeout=URL_TIMEOUT) - if resp.code == 200: - contents = resp.read() - return (resp.code, contents) - - from PyQt4.QtCore import QTimer - from PyQt4.QtGui import QMessageBox -else: - unicode = str - - from anki.sync import AnkiRequestsClient - def download(url): - contents = None - client = AnkiRequestsClient() - client.timeout = URL_TIMEOUT - resp = client.get(url) - if resp.status_code == 200: - contents = client.streamContent(resp) - return (resp.status_code, contents) - - from PyQt5.QtCore import QTimer - from PyQt5.QtWidgets import QMessageBox - - -def makeBytes(data): - return data.encode('utf-8') - - -def makeStr(data): - return data.decode('utf-8') - - -def api(*versions): - def decorator(func): - method = lambda *args, **kwargs: func(*args, **kwargs) - setattr(method, 'versions', versions) - setattr(method, 'api', True) - return method - - return decorator - - -# -# WebRequest -# - -class WebRequest: - def __init__(self, headers, body): - self.headers = headers - self.body = body - - -# -# WebClient -# - -class WebClient: - def __init__(self, sock, handler): - self.sock = sock - self.handler = handler - self.readBuff = bytes() - self.writeBuff = bytes() - - - def advance(self, recvSize=1024): - if self.sock is None: - return False - - rlist, wlist = select.select([self.sock], [self.sock], [], 0)[:2] - - if rlist: - msg = self.sock.recv(recvSize) - if not msg: - self.close() - return False - - self.readBuff += msg - - req, length = self.parseRequest(self.readBuff) - if req is not None: - self.readBuff = self.readBuff[length:] - self.writeBuff += self.handler(req) - - if wlist and self.writeBuff: - length = self.sock.send(self.writeBuff) - self.writeBuff = self.writeBuff[length:] - if not self.writeBuff: - self.close() - return False - - return True - - - def close(self): - if self.sock is not None: - self.sock.close() - self.sock = None - - self.readBuff = bytes() - self.writeBuff = bytes() - - - def parseRequest(self, data): - parts = data.split(makeBytes('\r\n\r\n'), 1) - if len(parts) == 1: - return None, 0 - - headers = {} - for line in parts[0].split(makeBytes('\r\n')): - pair = line.split(makeBytes(': ')) - headers[pair[0].lower()] = pair[1] if len(pair) > 1 else None - - headerLength = len(parts[0]) + 4 - bodyLength = int(headers.get(makeBytes('content-length'), 0)) - totalLength = headerLength + bodyLength - - if totalLength > len(data): - return None, 0 - - body = data[headerLength : totalLength] - return WebRequest(headers, body), totalLength - - -# -# WebServer -# - -class WebServer: - def __init__(self, handler): - self.handler = handler - self.clients = [] - self.sock = None - self.resetHeaders() - - - def setHeader(self, name, value): - self.headersOpt[name] = value - - - def resetHeaders(self): - self.headers = [ - ['HTTP/1.1 200 OK', None], - ['Content-Type', 'text/json'], - ['Access-Control-Allow-Origin', '*'] - ] - self.headersOpt = {} - - - def getHeaders(self): - headers = self.headers[:] - for name in self.headersOpt: - headers.append([name, self.headersOpt[name]]) - - return headers - - - def advance(self): - if self.sock is not None: - self.acceptClients() - self.advanceClients() - - - def acceptClients(self): - rlist = select.select([self.sock], [], [], 0)[0] - if not rlist: - return - - clientSock = self.sock.accept()[0] - if clientSock is not None: - clientSock.setblocking(False) - self.clients.append(WebClient(clientSock, self.handlerWrapper)) - - - def advanceClients(self): - self.clients = list(filter(lambda c: c.advance(), self.clients)) - - - def listen(self): - self.close() - - self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self.sock.setblocking(False) - self.sock.bind((NET_ADDRESS, NET_PORT)) - self.sock.listen(NET_BACKLOG) - - - def handlerWrapper(self, req): - if len(req.body) == 0: - body = makeBytes('AnkiConnect v.{}'.format(API_VERSION)) - else: - try: - params = json.loads(makeStr(req.body)) - body = makeBytes(json.dumps(self.handler(params))) - except ValueError: - body = makeBytes(json.dumps(None)) - - resp = bytes() - - self.setHeader('Content-Length', str(len(body))) - headers = self.getHeaders() - - for key, value in headers: - if value is None: - resp += makeBytes('{}\r\n'.format(key)) - else: - resp += makeBytes('{}: {}\r\n'.format(key, value)) - - resp += makeBytes('\r\n') - resp += body - - return resp - - - def close(self): - if self.sock is not None: - self.sock.close() - self.sock = None - - for client in self.clients: - client.close() - - self.clients = [] - - -# -# AnkiConnect -# - -class AnkiConnect: - def __init__(self): - self.server = WebServer(self.handler) - self.log = None - if API_LOG_PATH is not None: - self.log = open(API_LOG_PATH, 'w') - - try: - self.server.listen() - - self.timer = QTimer() - self.timer.timeout.connect(self.advance) - self.timer.start(TICK_INTERVAL) - except: - QMessageBox.critical( - self.window(), - 'AnkiConnect', - 'Failed to listen on port {}.\nMake sure it is available and is not in use.'.format(NET_PORT) - ) - - - def advance(self): - self.server.advance() - - - def handler(self, request): - if self.log is not None: - self.log.write('[request]\n') - json.dump(request, self.log, indent=4, sort_keys=True) - self.log.write('\n\n') - - name = request.get('action', '') - version = request.get('version', 4) - params = request.get('params', {}) - reply = {'result': None, 'error': None} - - try: - method = None - - for methodName, methodInst in inspect.getmembers(self, predicate=inspect.ismethod): - apiVersionLast = 0 - apiNameLast = None - - if getattr(methodInst, 'api', False): - for apiVersion, apiName in getattr(methodInst, 'versions', []): - if apiVersionLast < apiVersion <= version: - apiVersionLast = apiVersion - apiNameLast = apiName - - if apiNameLast is None and apiVersionLast == 0: - apiNameLast = methodName - - if apiNameLast is not None and apiNameLast == name: - method = methodInst - break - - if method is None: - raise Exception('unsupported action') - else: - reply['result'] = methodInst(**params) - except Exception as e: - reply['error'] = str(e) - - if version <= 4: - reply = reply['result'] - - if self.log is not None: - self.log.write('[reply]\n') - json.dump(reply, self.log, indent=4, sort_keys=True) - self.log.write('\n\n') - - return reply - - - def download(self, url): - try: - (code, contents) = download(url) - except Exception as e: - raise Exception('{} download failed with error {}'.format(url, str(e))) - if code == 200: - return contents - else: - raise Exception('{} download failed with return code {}'.format(url, code)) - - - def window(self): - return aqt.mw - - - def reviewer(self): - reviewer = self.window().reviewer - if reviewer is None: - raise Exception('reviewer is not available') - else: - return reviewer - - - def collection(self): - collection = self.window().col - if collection is None: - raise Exception('collection is not available') - else: - return collection - - - def decks(self): - decks = self.collection().decks - if decks is None: - raise Exception('decks are not available') - else: - return decks - - - def scheduler(self): - scheduler = self.collection().sched - if scheduler is None: - raise Exception('scheduler is not available') - else: - return scheduler - - - def database(self): - database = self.collection().db - if database is None: - raise Exception('database is not available') - else: - return database - - - def media(self): - media = self.collection().media - if media is None: - raise Exception('media is not available') - else: - return media - - - def startEditing(self): - self.window().requireReset() - - - def stopEditing(self): - if self.collection() is not None: - self.window().maybeReset() - - - def createNote(self, note): - collection = self.collection() - - model = collection.models.byName(note['modelName']) - if model is None: - raise Exception('model was not found: {}'.format(note['modelName'])) - - deck = collection.decks.byName(note['deckName']) - if deck is None: - raise Exception('deck was not found: {}'.format(note['deckName'])) - - ankiNote = anki.notes.Note(collection, model) - ankiNote.model()['did'] = deck['id'] - ankiNote.tags = note['tags'] - - for name, value in note['fields'].items(): - if name in ankiNote: - ankiNote[name] = value - - allowDuplicate = False - if 'options' in note: - if 'allowDuplicate' in note['options']: - allowDuplicate = note['options']['allowDuplicate'] - if type(allowDuplicate) is not bool: - raise Exception('option parameter \'allowDuplicate\' must be boolean') - - duplicateOrEmpty = ankiNote.dupeOrEmpty() - if duplicateOrEmpty == 1: - raise Exception('cannot create note because it is empty') - elif duplicateOrEmpty == 2: - if not allowDuplicate: - raise Exception('cannot create note because it is a duplicate') - else: - return ankiNote - elif duplicateOrEmpty == False: - return ankiNote - else: - raise Exception('cannot create note for unknown reason') - - - # - # Miscellaneous - # - - @api() - def version(self): - return API_VERSION - - - @api() - def upgrade(self): - response = QMessageBox.question( - self.window(), - 'AnkiConnect', - 'Upgrade to the latest version?', - QMessageBox.Yes | QMessageBox.No - ) - - if response == QMessageBox.Yes: - try: - data = self.download(URL_UPGRADE) - path = os.path.splitext(__file__)[0] + '.py' - with open(path, 'w') as fp: - fp.write(makeStr(data)) - QMessageBox.information( - self.window(), - 'AnkiConnect', - 'Upgraded to the latest version, please restart Anki.' - ) - return True - except Exception as e: - QMessageBox.critical(self.window(), 'AnkiConnect', 'Failed to download latest version.') - raise e - - return False - - - @api() - def loadProfile(self, name): - if name not in self.window().pm.profiles(): - return False - if not self.window().isVisible(): - self.window().pm.load(name) - self.window().loadProfile() - self.window().profileDiag.closeWithoutQuitting() - else: - cur_profile = self.window().pm.name - if cur_profile != name: - self.window().unloadProfileAndShowProfileManager() - self.loadProfile(name) - return True - - - @api() - def sync(self): - self.window().onSync() - - - @api() - def multi(self, actions): - return list(map(self.handler, actions)) - - - # - # Decks - # - - @api() - def deckNames(self): - return self.decks().allNames() - - - @api() - def deckNamesAndIds(self): - decks = {} - for deck in self.deckNames(): - decks[deck] = self.decks().id(deck) - - return decks - - - @api() - def getDecks(self, cards): - decks = {} - for card in cards: - did = self.database().scalar('select did from cards where id=?', card) - deck = self.decks().get(did)['name'] - if deck in decks: - decks[deck].append(card) - else: - decks[deck] = [card] - - return decks - - - @api() - def createDeck(self, deck): - try: - self.startEditing() - did = self.decks().id(deck) - finally: - self.stopEditing() - - return did - - - @api() - def changeDeck(self, cards, deck): - self.startEditing() - - did = self.collection().decks.id(deck) - mod = anki.utils.intTime() - usn = self.collection().usn() - - # normal cards - scids = anki.utils.ids2str(cards) - # remove any cards from filtered deck first - self.collection().sched.remFromDyn(cards) - - # then move into new deck - self.collection().db.execute('update cards set usn=?, mod=?, did=? where id in ' + scids, usn, mod, did) - self.stopEditing() - - - @api() - def deleteDecks(self, decks, cardsToo=False): - try: - self.startEditing() - decks = filter(lambda d: d in self.deckNames(), decks) - for deck in decks: - did = self.decks().id(deck) - self.decks().rem(did, cardsToo) - finally: - self.stopEditing() - - - @api() - def getDeckConfig(self, deck): - if not deck in self.deckNames(): - return False - - collection = self.collection() - did = collection.decks.id(deck) - return collection.decks.confForDid(did) - - - @api() - def saveDeckConfig(self, config): - collection = self.collection() - - config['id'] = str(config['id']) - config['mod'] = anki.utils.intTime() - config['usn'] = collection.usn() - - if not config['id'] in collection.decks.dconf: - return False - - collection.decks.dconf[config['id']] = config - collection.decks.changed = True - return True - - - @api() - def setDeckConfigId(self, decks, configId): - configId = str(configId) - for deck in decks: - if not deck in self.deckNames(): - return False - - collection = self.collection() - if not configId in collection.decks.dconf: - return False - - for deck in decks: - did = str(collection.decks.id(deck)) - aqt.mw.col.decks.decks[did]['conf'] = configId - - return True - - - @api() - def cloneDeckConfigId(self, name, cloneFrom='1'): - configId = str(cloneFrom) - if not configId in self.collection().decks.dconf: - return False - - config = self.collection().decks.getConf(configId) - return self.collection().decks.confId(name, config) - - - @api() - def removeDeckConfigId(self, configId): - configId = str(configId) - collection = self.collection() - if configId == 1 or not configId in collection.decks.dconf: - return False - - collection.decks.remConf(configId) - return True - - - @api() - def storeMediaFile(self, filename, data): - self.deleteMediaFile(filename) - self.media().writeData(filename, base64.b64decode(data)) - - - @api() - def retrieveMediaFile(self, filename): - filename = os.path.basename(filename) - filename = normalize('NFC', filename) - filename = self.media().stripIllegal(filename) - - path = os.path.join(self.media().dir(), filename) - if os.path.exists(path): - with open(path, 'rb') as file: - return base64.b64encode(file.read()).decode('ascii') - - return False - - - @api() - def deleteMediaFile(self, filename): - self.media().syncDelete(filename) - - - @api() - def addNote(self, note): - ankiNote = self.createNote(note) - - audio = note.get('audio') - if audio is not None and len(audio['fields']) > 0: - try: - data = self.download(audio['url']) - skipHash = audio.get('skipHash') - if skipHash is None: - skip = False - else: - m = hashlib.md5() - m.update(data) - skip = skipHash == m.hexdigest() - - if not skip: - for field in audio['fields']: - if field in ankiNote: - ankiNote[field] += u'[sound:{}]'.format(audio['filename']) - - self.media().writeData(audio['filename'], data) - except Exception as e: - errorMessage = str(e).replace("&", "&").replace("<", "<").replace(">", ">") - for field in audio['fields']: - if field in ankiNote: - ankiNote[field] += errorMessage - - collection = self.collection() - self.startEditing() - collection.addNote(ankiNote) - collection.autosave() - self.stopEditing() - - return ankiNote.id - - - @api() - def canAddNote(self, note): - try: - return bool(self.createNote(note)) - except: - return False - - - @api() - def updateNoteFields(self, note): - ankiNote = self.collection().getNote(note['id']) - if ankiNote is None: - raise Exception('note was not found: {}'.format(note['id'])) - - for name, value in note['fields'].items(): - if name in ankiNote: - ankiNote[name] = value - - ankiNote.flush() - - - @api() - def addTags(self, notes, tags, add=True): - self.startEditing() - self.collection().tags.bulkAdd(notes, tags, add) - self.stopEditing() - - - @api() - def removeTags(self, notes, tags): - return self.addTags(notes, tags, False) - - - @api() - def getTags(self): - return self.collection().tags.all() - - - @api() - def suspend(self, cards, suspend=True): - for card in cards: - if self.suspended(card) == suspend: - cards.remove(card) - - if len(cards) == 0: - return False - - scheduler = self.scheduler() - self.startEditing() - if suspend: - scheduler.suspendCards(cards) - else: - scheduler.unsuspendCards(cards) - self.stopEditing() - - return True - - - @api() - def unsuspend(self, cards): - self.suspend(cards, False) - - - @api() - def suspended(self, card): - card = self.collection().getCard(card) - return card.queue == -1 - - - @api() - def areSuspended(self, cards): - suspended = [] - for card in cards: - suspended.append(self.suspended(card)) - - return suspended - - - @api() - def areDue(self, cards): - due = [] - for card in cards: - if self.findCards('cid:{} is:new'.format(card)): - due.append(True) - else: - date, ivl = self.collection().db.all('select id/1000.0, ivl from revlog where cid = ?', card)[-1] - if ivl >= -1200: - due.append(bool(self.findCards('cid:{} is:due'.format(card)))) - else: - due.append(date - ivl <= time()) - - return due - - - @api() - def getIntervals(self, cards, complete=False): - intervals = [] - for card in cards: - if self.findCards('cid:{} is:new'.format(card)): - intervals.append(0) - else: - interval = self.collection().db.list('select ivl from revlog where cid = ?', card) - if not complete: - interval = interval[-1] - intervals.append(interval) - - return intervals - - - - @api() - def modelNames(self): - return self.collection().models.allNames() - - - @api() - def modelNamesAndIds(self): - models = {} - for model in self.modelNames(): - models[model] = int(self.collection().models.byName(model)['id']) - - return models - - - @api() - def modelNameFromId(self, modelId): - model = self.collection().models.get(modelId) - if model is None: - raise Exception('model was not found: {}'.format(modelId)) - else: - return model['name'] - - - @api() - def modelFieldNames(self, modelName): - model = self.collection().models.byName(modelName) - if model is None: - raise Exception('model was not found: {}'.format(modelName)) - else: - return [field['name'] for field in model['flds']] - - - @api() - def modelFieldsOnTemplates(self, modelName): - model = self.collection().models.byName(modelName) - if model is None: - raise Exception('model was not found: {}'.format(modelName)) - - templates = {} - for template in model['tmpls']: - fields = [] - for side in ['qfmt', 'afmt']: - fieldsForSide = [] - - # based on _fieldsOnTemplate from aqt/clayout.py - matches = re.findall('{{[^#/}]+?}}', template[side]) - for match in matches: - # remove braces and modifiers - match = re.sub(r'[{}]', '', match) - match = match.split(':')[-1] - - # for the answer side, ignore fields present on the question side + the FrontSide field - if match == 'FrontSide' or side == 'afmt' and match in fields[0]: - continue - fieldsForSide.append(match) - - fields.append(fieldsForSide) - - templates[template['name']] = fields - - return templates - - - @api() - def deckNameFromId(self, deckId): - deck = self.collection().decks.get(deckId) - if deck is None: - raise Exception('deck was not found: {}'.format(deckId)) - else: - return deck['name'] - - - @api() - def findNotes(self, query=None): - if query is None: - return [] - else: - return self.collection().findNotes(query) - - - @api() - def findCards(self, query=None): - if query is None: - return [] - else: - return self.collection().findCards(query) - - - @api() - def cardsInfo(self, cards): - result = [] - for cid in cards: - try: - card = self.collection().getCard(cid) - model = card.model() - note = card.note() - fields = {} - for info in model['flds']: - order = info['ord'] - name = info['name'] - fields[name] = {'value': note.fields[order], 'order': order} - - result.append({ - 'cardId': card.id, - 'fields': fields, - 'fieldOrder': card.ord, - 'question': card._getQA()['q'], - 'answer': card._getQA()['a'], - 'modelName': model['name'], - 'deckName': self.deckNameFromId(card.did), - 'css': model['css'], - 'factor': card.factor, - #This factor is 10 times the ease percentage, - # so an ease of 310% would be reported as 3100 - 'interval': card.ivl, - 'note': card.nid - }) - except TypeError as e: - # Anki will give a TypeError if the card ID does not exist. - # Best behavior is probably to add an 'empty card' to the - # returned result, so that the items of the input and return - # lists correspond. - result.append({}) - - return result - - - @api() - def notesInfo(self, notes): - result = [] - for nid in notes: - try: - note = self.collection().getNote(nid) - model = note.model() - - fields = {} - for info in model['flds']: - order = info['ord'] - name = info['name'] - fields[name] = {'value': note.fields[order], 'order': order} - - result.append({ - 'noteId': note.id, - 'tags' : note.tags, - 'fields': fields, - 'modelName': model['name'], - 'cards': self.collection().db.list('select id from cards where nid = ? order by ord', note.id) - }) - except TypeError as e: - # Anki will give a TypeError if the note ID does not exist. - # Best behavior is probably to add an 'empty card' to the - # returned result, so that the items of the input and return - # lists correspond. - result.append({}) - - return result - - - - - - @api() - def cardsToNotes(self, cards): - return self.collection().db.list('select distinct nid from cards where id in ' + anki.utils.ids2str(cards)) - - - @api() - def guiBrowse(self, query=None): - browser = aqt.dialogs.open('Browser', self.window()) - browser.activateWindow() - - if query is not None: - browser.form.searchEdit.lineEdit().setText(query) - if hasattr(browser, 'onSearch'): - browser.onSearch() - else: - browser.onSearchActivated() - - return browser.model.cards - - - @api() - def guiAddCards(self, note=None): - - if note is not None: - collection = self.collection() - - deck = collection.decks.byName(note['deckName']) - if deck is None: - raise Exception('deck was not found: {}'.format(note['deckName'])) - - self.collection().decks.select(deck['id']) - - model = collection.models.byName(note['modelName']) - if model is None: - raise Exception('model was not found: {}'.format(note['modelName'])) - - self.collection().models.setCurrent(model) - self.collection().models.update(model) - - closeAfterAdding = False - if note is not None and 'options' in note: - if 'closeAfterAdding' in note['options']: - closeAfterAdding = note['options']['closeAfterAdding'] - if type(closeAfterAdding) is not bool: - raise Exception('option parameter \'closeAfterAdding\' must be boolean') - - addCards = 'foobar' - - if closeAfterAdding: - - randomString = ''.join(choice(ascii_letters) for _ in range(10)) - windowName = 'AddCardsAndClose' + randomString - - if ANKI21: - class AddCardsAndClose(aqt.addcards.AddCards): - - def __init__(self, mw): - super(AddCardsAndClose, self).__init__(mw) - self.addButton.setText("Add and Close") - self.addButton.setShortcut(aqt.qt.QKeySequence("Ctrl+Return")) - - def _addCards(self): - super(AddCardsAndClose, self)._addCards() - self.reject() - - def _reject(self): - savedMarkClosed = aqt.dialogs.markClosed - aqt.dialogs.markClosed = lambda _: savedMarkClosed(windowName) - super(AddCardsAndClose, self)._reject() - aqt.dialogs.markClosed = savedMarkClosed - - else: - class AddCardsAndClose(aqt.addcards.AddCards): - - def __init__(self, mw): - super(AddCardsAndClose, self).__init__(mw) - self.addButton.setText("Add and Close") - self.addButton.setShortcut(aqt.qt.QKeySequence("Ctrl+Return")) - - def addCards(self): - super(AddCardsAndClose, self)._addCards() - self.reject() - - def reject(self): - savedMarkClosed = aqt.dialogs.markClosed - aqt.dialogs.markClosed = lambda _: savedMarkClosed(windowName) - super(AddCardsAndClose, self)._reject() - aqt.dialogs.markClosed = savedMarkClosed - - aqt.dialogs._dialogs[windowName] = [AddCardsAndClose, None] - addCards = aqt.dialogs.open(windowName, self.window()) - - editor = addCards.editor - ankiNote = editor.note - - if 'fields' in note: - for name, value in note['fields'].items(): - if name in ankiNote: - ankiNote[name] = value - editor.loadNote() - - if 'tags' in note: - ankiNote.tags = note['tags'] - editor.updateTags() - - # if Anki does not Focus, the window will not notice that the - # fields are actually filled - aqt.dialogs.open(windowName, self.window()) - if ANKI21: - addCards.setAndFocusNote(editor.note) - - elif note is not None: - currentWindow = aqt.dialogs._dialogs['AddCards'][1] - - def openNewWindow(): - addCards = aqt.dialogs.open('AddCards', self.window()) - - editor = addCards.editor - ankiNote = editor.note - - # we have to fill out the card in the callback - # otherwise we can't assure, the new window is open - if 'fields' in note: - for name, value in note['fields'].items(): - if name in ankiNote: - ankiNote[name] = value - editor.loadNote() - - if 'tags' in note: - ankiNote.tags = note['tags'] - editor.updateTags() - - addCards.activateWindow() - - aqt.dialogs.open('AddCards', self.window()) - if ANKI21: - addCards.setAndFocusNote(editor.note) - - if currentWindow is not None: - currentWindow.closeWithCallback(openNewWindow) - else: - openNewWindow() - - else: - addCards = aqt.dialogs.open('AddCards', self.window()) - addCards.activateWindow() - - @api() - def guiReviewActive(self): - return self.reviewer().card is not None and self.window().state == 'review' - - - @api() - def guiCurrentCard(self): - if not self.guiReviewActive(): - raise Exception('Gui review is not currently active.') - - reviewer = self.reviewer() - card = reviewer.card - model = card.model() - note = card.note() - - fields = {} - for info in model['flds']: - order = info['ord'] - name = info['name'] - fields[name] = {'value': note.fields[order], 'order': order} - - if card is not None: - return { - 'cardId': card.id, - 'fields': fields, - 'fieldOrder': card.ord, - 'question': card._getQA()['q'], - 'answer': card._getQA()['a'], - 'buttons': [b[0] for b in reviewer._answerButtonList()], - 'modelName': model['name'], - 'deckName': self.deckNameFromId(card.did), - 'css': model['css'], - 'template': card.template()['name'] - } - - - @api() - def guiStartCardTimer(self): - if not self.guiReviewActive(): - return False - - card = self.reviewer().card - - if card is not None: - card.startTimer() - return True - else: - return False - - - @api() - def guiShowQuestion(self): - if self.guiReviewActive(): - self.reviewer()._showQuestion() - return True - else: - return False - - - @api() - def guiShowAnswer(self): - if self.guiReviewActive(): - self.window().reviewer._showAnswer() - return True - else: - return False - - - @api() - def guiAnswerCard(self, ease): - if not self.guiReviewActive(): - return False - - reviewer = self.reviewer() - if reviewer.state != 'answer': - return False - if ease <= 0 or ease > self.scheduler().answerButtons(reviewer.card): - return False - - reviewer._answerCard(ease) - return True - - - @api() - def guiDeckOverview(self, name): - collection = self.collection() - if collection is not None: - deck = collection.decks.byName(name) - if deck is not None: - collection.decks.select(deck['id']) - self.window().onOverview() - return True - - return False - - - @api() - def guiDeckBrowser(self): - self.window().moveToState('deckBrowser') - - - @api() - def guiDeckReview(self, name): - if self.guiDeckOverview(name): - self.window().moveToState('review') - return True - else: - return False - - - @api() - def guiExitAnki(self): - timer = QTimer() - def exitAnki(): - timer.stop() - self.window().close() - timer.timeout.connect(exitAnki) - timer.start(1000) # 1s should be enough to allow the response to be sent. - - - - @api() - def addNotes(self, notes): - results = [] - for note in notes: - try: - results.append(self.addNote(note)) - except Exception: - results.append(None) - - return results - - - @api() - def canAddNotes(self, notes): - results = [] - for note in notes: - results.append(self.canAddNote(note)) - - return results - - -# -# Entry -# - -ac = AnkiConnect()