class PanelInputLine(QLineEdit): down_pressed = pyqtSignal() up_pressed = pyqtSignal() im_changed = pyqtSignal(str) def __init__(self, parent=None): super().__init__(parent) if not self.testAttribute(Qt.WidgetAttribute.WA_InputMethodEnabled): self.setAttribute(Qt.WidgetAttribute.WA_InputMethodEnabled) def inputMethodEvent(self, event): super().inputMethodEvent(event) self.im_changed.emit(self.text() + event.preeditString()) def keyPressEvent(self, event): super().keyPressEvent(event) mod = mw.app.keyboardModifiers() & Qt.KeyboardModifier.ControlModifier key = event.key() if key == Qt.Key.Key_Down: self.down_pressed.emit() elif key == Qt.Key.Key_Up: self.up_pressed.emit() elif mod and (key == Qt.Key.Key_N): self.down_pressed.emit() elif mod and (key == Qt.Key.Key_P): self.up_pressed.emit() elif mod and (key == Qt.Key.Key_H): self.up_pressed.emit()
class LemmasRequester(QThread): """Auxiliary class for receiving lemmas in seperated thread""" lemmasReceived = pyqtSignal(list) suggestionsReceived = pyqtSignal(list) exceptionReceived = pyqtSignal() def __init__(self, lemma_query, query_kargs): QThread.__init__(self) self.lemma_query = lemma_query self.query_kargs = query_kargs def run(self): """ If lemma list correctly received then emit lemmasReceived If suggestions received then emit suggestionsReceived if exception Exception raised then emit exceptionReceived """ try: responce_type, responce = query.query(str(self.lemma_query), **self.query_kargs) #aqt.utils.showInfo('responce_type "{}"; responce "{}"'.format(responce_type, responce)) #aqt.utils.showInfo('{}'.format(str(self.lemma_query))) if responce_type == 'word_id': lemmas = query.query_lemma(responce, **self.query_kargs) self.lemmasReceived.emit(lemmas) else: suggestions = query.query_suggestions(responce, **self.query_kargs) self.suggestionsReceived.emit(suggestions) except Exception as exception: sys.stderr.write(traceback.format_exc()) self.exceptionReceived.emit()
class SyncDialog(qt.QDialog): syncClicked = qt.pyqtSignal() confirmClicked = qt.pyqtSignal(list, qt.QDialog) def __init__(self, parent): super().__init__(parent) self.initUI() def initUI(self): self.layout = qt.QFormLayout() self.layout.setFieldGrowthPolicy(qt.QFormLayout.ExpandingFieldsGrow) self.setLayout(self.layout) self.extendedQueryEdit = qt.QLineEdit() self.layout.addRow('Extended query', self.extendedQueryEdit) self.skipTaggedCheckBox = qt.QCheckBox() self.skipTaggedCheckBox.setChecked(True) self.layout.addRow('Skip tagged', self.skipTaggedCheckBox) self.resolveManuallyCheckBox = qt.QCheckBox() self.layout.addRow('Resolve manually', self.resolveManuallyCheckBox) self.syncButton = qt.QPushButton('Sync') self.syncButton.clicked.connect(self.syncClicked) self.layout.addRow(self.syncButton) self.confirmDialog = ConfirmDialog(self) self.confirmDialog.confirmClicked.connect(self.onConfirmClicked) def exec_(self): self.syncButton.setFocus() super().exec_() def showConfirmDialog(self, changeOperations): self.close() self.confirmDialog.exec_(changeOperations) def onConfirmClicked(self, changeOperations): self.confirmClicked.emit(changeOperations, self.confirmDialog) self.confirmDialog.close() def skipTagged(self): return self.skipTaggedCheckBox.isChecked() def resolveManually(self): return self.resolveManuallyCheckBox.isChecked() def extendedQuery(self): return self.extendedQueryEdit.text()
class LoginStateCheckWorker(QObject): start = pyqtSignal() logSuccess = pyqtSignal(str) logFailed = pyqtSignal() def __init__(self, checkFn, cookie): super().__init__() self.checkFn = checkFn self.cookie = cookie def run(self): loginState = self.checkFn(self.cookie) if loginState: self.logSuccess.emit(json.dumps(self.cookie)) else: self.logFailed.emit()
class ImportThread(QThread): """ Background thread to export a wiki and parse questions out of it. """ progress_update = pyqtSignal(int, int) def __init__(self, conf: dict, wiki_name: str, wiki_conf: Dict[str, str]) -> None: super().__init__() self.conf = conf self.wiki_name = wiki_name self.wiki_conf = wiki_conf self.notes: Optional[Set[TwNote]] = None self.exception: Optional[Exception] = None self.warnings: List[str] = [] def run(self) -> None: "Find notes, updating owner on progress periodically." try: self.notes = twimport.find_notes( tw_binary=self.conf['tiddlywikiBinary'], wiki_path=self.wiki_conf['path'], wiki_type=self.wiki_conf['type'], wiki_name=self.wiki_name, filter_=self.wiki_conf['contentFilter'], password=self.wiki_conf.get('password', ''), callback=self.progress_update.emit, warnings=self.warnings, ) for n in self.notes: wiki_url = self.wiki_conf.get('permalink', '') if wiki_url: n.set_permalink(wiki_url) except Exception as e: self.exception = e
class QueryWorker(QThread): result_ready = pyqtSignal(dict) progress_update = pyqtSignal(dict) def __init__(self, service_unique): super(QueryWorker, self).__init__() self.service_unique = service_unique self.index = 0 self.service = service_manager.get_service(service_unique) self.completed_counts = 0 self.queue = Queue() self.result_ready.connect(handle_results) self.progress_update.connect(progress.update_labels) def target(self, index, service_field, word): self.queue.put((index, service_field, word)) def run(self): # self.completed_counts = 0 while True: if progress.abort(): break try: index, service_field, word = self.queue.get(timeout=0.1) # self.progress_update.emit({ # 'service_name': self.service.title, # 'word': word, # 'field_name': service_field # }) result = self.query(service_field, word) self.result_ready.emit({index: result}) self.completed_counts += 1 # rest a moment self.rest() except Empty: break def rest(self): time.sleep(self.service.query_interval) def query(self, service_field, word): self.service.set_notifier(self.progress_update, self.index) return self.service.active(service_field, word)
class Worker(qt.QObject): """Worker to get the IPA transcriptions of the selected Anki notes.""" finished = qt.pyqtSignal() progress_changed = qt.pyqtSignal(int) result = qt.pyqtSignal(dict) def __init__(self, notes: Dict[int, anki.notes.Note], lang: str, base_field: str) -> None: """ Initialize Worker. :param notes: Anki notes we want to use :param lang: language of base field content :param base_field: field for which we want to get IPA transcriptions """ super().__init__() self.notes = notes self.lang = lang self.base_field = base_field self._isRunning = True @qt.pyqtSlot() def run(self) -> None: """Get IPA transcription for each note and save it into a dictionary.""" new_dict = dict() for index, key in enumerate(self.notes.keys()): try: if self.lang == "english": new_dict[key] = get_english_ipa_transcription(self.notes[key][self.base_field]) else: words = utils.get_words_from_field(field_text=self.notes[key][self.base_field]) new_dict[key] = parse_ipa_transcription.transcript(words=words, language=self.lang) # IPA transcription not found except (urllib.error.HTTPError, IndexError): continue self.progress_changed.emit(index) self.result.emit(new_dict) self.finished.emit() def stop(self) -> None: """Stop worker.""" self._isRunning = False
class LatestVersionFinder(QThread): newVerAvail = pyqtSignal(str) newMsg = pyqtSignal(dict) clockIsOff = pyqtSignal(float) def __init__(self, main): QThread.__init__(self) self.main = main self.config = main.pm.meta def _data(self): d = { "ver": versionWithBuild(), "os": platDesc(), "id": self.config['id'], "lm": self.config['lastMsg'], "crt": self.config['created'] } return d def run(self): if not self.config['updates']: return d = self._data() d['proto'] = 1 try: r = requests.post(aqt.appUpdate, data=d) r.raise_for_status() resp = r.json() except: # behind proxy, corrupt message, etc print("update check failed") return if resp['msg']: self.newMsg.emit(resp) if resp['ver']: self.newVerAvail.emit(resp['ver']) diff = resp['time'] - time.time() if abs(diff) > 300: self.clockIsOff.emit(diff)
class WordDownloadWorker(QObject): start = pyqtSignal() tick = pyqtSignal() done = pyqtSignal() logger = logging.getLogger(__name__ + '.WordDownloadWorker') def __init__(self, api): super().__init__() self.api = api def run(self): currentThread = QThread.currentThread() for word in self.api.getAllWords(): if currentThread.isInterruptionRequested(): return self.api.insertWord(word) self.tick.emit() self.done.emit()
class WordExampleDownloadWorker(QObject): start = pyqtSignal() tick = pyqtSignal() done = pyqtSignal() logger = logging.getLogger(__name__ + '.WordExampleDownloadWorker') def __init__(self, api): super().__init__() self.api = api def run(self): currentThread = QThread.currentThread() for row in self.api.getWordsWithoutExample(): if currentThread.isInterruptionRequested(): return self.api.insertWordExamples(row['id']) self.tick.emit() self.done.emit()
class SentenceTranslateDownloadWorker(QObject): start = pyqtSignal() tick = pyqtSignal() done = pyqtSignal() logger = logging.getLogger(__name__ + '.SentenceTranslateDownloadWorker') def __init__(self, api): super().__init__() self.api = api def run(self): currentThread = QThread.currentThread() for row in self.api.getSentencesWithoutTranslate(): if currentThread.isInterruptionRequested(): return self.api.insertSentenceTranslates(row) self.tick.emit() self.done.emit()
class QueryWorker(QThread): result_ready = pyqtSignal(dict) progress_update = pyqtSignal(dict) def __init__(self, service_name, type): super(QueryWorker, self).__init__() self.service_name = service_name self.queue = Queue() if type == 'web': self.service = web_service_manager.get_service(service_name) if type == 'mdx': self.service = mdx_service_manager.get_service(service_name) self.result_ready.connect(handle_results) self.progress_update.connect(update_progress_label) def target(self, index, service_field, word): self.queue.put((index, service_field, word)) def run(self): # try: while True: try: index, service_field, word = self.queue.get(timeout=0.1) name_info = os.path.basename( self.service_name) if os.path.isabs( self.service_name) else self.service_name self.progress_update.emit({ 'service_name': name_info, 'word': word, 'field_name': service_field }) result = self.query(service_field, word) if self.service else "" # showInfo('%d, %s' % (index, str(result))) self.result_ready.emit({index: result}) except Empty: break def query(self, service_field, word): return self.service.instance.active(service_field, word)
class ConfigSignals(QObject): initialized = pyqtSignal() saved = pyqtSignal() loaded = pyqtSignal() reset = pyqtSignal() deleted = pyqtSignal() unloaded = pyqtSignal()
class AudioDownloadWorker(QObject): start = pyqtSignal() tick = pyqtSignal() done = pyqtSignal() logger = logging.getLogger(__name__ + '.AudioDownloadWorker') retries = Retry(total=5, backoff_factor=3, status_forcelist=[500, 502, 503, 504]) session = requests.Session() session.mount('http://', HTTPAdapter(max_retries=retries)) session.mount('https://', HTTPAdapter(max_retries=retries)) def __init__(self, audios: [tuple]): super().__init__() self.audios = audios def run(self): currentThread = QThread.currentThread() def __download(fileName, url): try: if currentThread.isInterruptionRequested(): return r = self.session.get(url, stream=True) if not path.exists(fileName) or path.getsize(fileName) != int(r.headers['Content-Length']): with open(fileName, 'wb') as f: for chunk in r.iter_content(chunk_size=1024): if chunk: f.write(chunk) self.logger.info(f'{fileName} 下载完成') else: self.logger.info(f'{fileName} 跳过下载') except Exception as e: self.logger.warning(f'下载{fileName}:{url}异常: {e}') finally: self.tick.emit() with ThreadPool(max_workers=3) as executor: for fileName, url in self.audios: executor.submit(__download, fileName, url) self.done.emit()
class ProfileManager(QMainWindow): onClose = pyqtSignal() closeFires = True def closeEvent(self, evt): if self.closeFires: self.onClose.emit() evt.accept() def closeWithoutQuitting(self): self.closeFires = False self.close() self.closeFires = True
class ConfirmDialog(qt.QDialog): confirmClicked = qt.pyqtSignal(list) def __init__(self, parent): super().__init__(parent) self.initUI() self.resetChangeOperations() def initUI(self): self.layout = qt.QVBoxLayout() self.setLayout(self.layout) self.table = ConfirmTable() self.layout.addWidget(self.table) self.hideUnchangedCheckBox = qt.QCheckBox('Hide unchanged') self.hideUnchangedCheckBox.stateChanged.connect(self.updateTable) self.layout.addWidget(self.hideUnchangedCheckBox) self.confirmButton = qt.QPushButton('Confirm') self.confirmButton.clicked.connect(self.onConfirmClicked) self.layout.addWidget(self.confirmButton) def resetChangeOperations(self): self.setChangeOperations([]) def updateTable(self): changeOperations = self.changeOperations if self.hideUnchangedCheckBox.isChecked(): changeOperations = list(filter(ChangeOperation.hasAnyChanges, changeOperations)) self.table.setChangeOperations(changeOperations) def setChangeOperations(self, changeOperations): self.changeOperations = changeOperations self.updateTable() def onConfirmClicked(self): self.confirmClicked.emit(self.changeOperations) def exec_(self, changeOperations): self.setChangeOperations(changeOperations) self.setWindowState(qt.Qt.WindowMaximized) self.confirmButton.setFocus() super().exec_() def close(self): super().close() self.resetChangeOperations()
class MigakuServerThread(QThread): alertUser = pyqtSignal(str) exportingCondensed = pyqtSignal() notExportingCondensed = pyqtSignal() def __init__(self, mw): self.mw = mw QThread.__init__(self) self.server = MigakuHTTPServer(self, mw) self.start() def run(self): asyncio.set_event_loop(asyncio.new_event_loop()) self.server.run() def alert(self, message): self.alertUser.emit(message) def addCondensedAudioInProgressMessage(self): self.exportingCondensed.emit() def removeCondensedAudioInProgressMessage(self): self.notExportingCondensed.emit()
class Handler(QObject, logging.Handler): newRecord = pyqtSignal(object) def __init__(self, parent): super().__init__(parent) super(logging.Handler).__init__() formatter = logging.Formatter( '[%(asctime)s][%(name)s][%(levelname)s]\n%(message)s\n', '%d/%m/%Y %H:%M:%S') self.setFormatter(formatter) self.setLevel(logging.DEBUG) def emit(self, record): msg = self.format(record) self.newRecord.emit(msg)
class Downloader(QThread): recv = pyqtSignal() def __init__(self, code): QThread.__init__(self) self.code = code self.error = None def run(self): # setup progress handler self.byteUpdate = time.time() self.recvTotal = 0 def recvEvent(bytes): self.recvTotal += bytes self.recv.emit() addHook("httpRecv", recvEvent) client = AnkiRequestsClient() try: resp = client.get(appShared + "download/%s?v=2.1" % self.code) if resp.status_code == 200: data = client.streamContent(resp) elif resp.status_code in (403, 404): self.error = _( "Invalid code, or add-on not available for your version of Anki." ) return else: self.error = _("Unexpected response code: %s" % resp.status_code) return except Exception as e: self.error = _( "Please check your internet connection.") + "\n\n" + str(e) return finally: remHook("httpRecv", recvEvent) self.fname = re.match("attachment; filename=(.+)", resp.headers['content-disposition']).group(1) self.data = data
class StatusListeningHttpClient(QObject): status_occurred = pyqtSignal(object) def __init__(self, http_client, status, on_status, parent=None): super().__init__(parent) self._http_client = http_client self._status = status self.status_occurred.connect(on_status) def _do_request(self, method, **kwargs): response = getattr(self._http_client, method)(**kwargs) if response.status_code == self._status: self.status_occurred.emit(response) return response get = partialmethod(_do_request, 'get') post = partialmethod(_do_request, 'post') put = partialmethod(_do_request, 'put') delete = partialmethod(_do_request, 'delete')
class SyncThread(QThread): event = pyqtSignal(str, str) def __init__(self, path, hkey, auth=None, media=True, 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 def flagAbort(self): self._abort = 1 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, log=True) except: self.fireEvent("corrupt") return self.server = RemoteServer(self.hkey, hostNum=self.hostNum) self.client = Syncer(self.col, self.server) self.sentTotal = 0 self.recvTotal = 0 def syncEvent(type): self.fireEvent("sync", type) def syncMsg(msg): self.fireEvent("syncMsg", msg) def sendEvent(bytes): if not self._abort: self.sentTotal += bytes self.fireEvent("send", str(self.sentTotal)) elif self._abort == 1: self._abort = 2 raise Exception("sync cancelled") def recvEvent(bytes): if not self._abort: self.recvTotal += bytes self.fireEvent("recv", str(self.recvTotal)) elif self._abort == 1: self._abort = 2 raise Exception("sync cancelled") addHook("sync", syncEvent) addHook("syncMsg", syncMsg) addHook("httpSend", sendEvent) addHook("httpRecv", recvEvent) # run sync and catch any errors try: self._sync() except: err = traceback.format_exc() self.fireEvent("error", err) finally: # don't bump mod time unless we explicitly save self.col.close(save=False) remHook("sync", syncEvent) remHook("syncMsg", syncMsg) remHook("httpSend", sendEvent) remHook("httpRecv", recvEvent) def _abortingSync(self): try: return self.client.sync() except Exception as e: if "sync cancelled" in str(e): self.server.abort() raise else: raise def _sync(self): if self.auth: # need to authenticate and obtain host key self.hkey = self.server.hostKey(*self.auth) if not self.hkey: # provided details were invalid return self.fireEvent("badAuth") else: # write new details and tell calling thread to save self.fireEvent("newKey", self.hkey) # run sync and check state try: ret = self._abortingSync() except Exception as e: log = traceback.format_exc() err = repr(str(e)) if ("Unable to find the server" in err or "Errno 2" in err or "getaddrinfo" in err): self.fireEvent("offline") elif "sync cancelled" in err: pass else: self.fireEvent("error", log) return if ret == "badAuth": return self.fireEvent("badAuth") elif ret == "clockOff": return self.fireEvent("clockOff") elif ret == "basicCheckFailed" or ret == "sanityCheckFailed": return self.fireEvent("checkFailed") # full sync? if ret == "fullSync": return self._fullSync() # save and note success state if ret == "noChanges": self.fireEvent("noChanges") elif ret == "success": self.fireEvent("success") elif ret == "serverAbort": pass else: self.fireEvent("error", "Unknown sync return code.") 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 # wait for a reply self.fullSyncChoice = False self.localIsEmpty = self.col.isEmpty() self.fireEvent("fullSync") while not self.fullSyncChoice: time.sleep(0.1) f = self.fullSyncChoice if f == "cancel": return self.client = FullSyncer(self.col, self.hkey, self.server.client, hostNum=self.hostNum) try: if f == "upload": if not self.client.upload(): self.fireEvent("upbad") else: ret = self.client.download() if ret == "downloadClobber": self.fireEvent(ret) return except Exception as e: 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)
class Forvo(QThread): resultsFound = pyqtSignal(list) def __init__(self, language): QThread.__init__(self) self.selLang = language self.term = False self.langShortCut = languages[self.selLang] self.GOOGLE_SEARCH_URL = "https://forvo.com/word/◳t/#" + self.langShortCut #◳r self.session = requests.session() self.session.headers.update( { "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:10.0) \ Gecko/20100101 Firefox/10.0" } ) def setTermIdName(self, term, idName): self.term = term self.idName = idName def run(self): if self.term: resultList = [self.attemptFetchForvoLinks(self.term), self.idName] self.resultsFound.emit(resultList) def search(self, term, lang = False): if lang and self.selLang != lang: self.selLang = lang self.langShortCut = languages[self.selLang] self.GOOGLE_SEARCH_URL = "https://forvo.com/word/◳t/#" + self.langShortCut query = self.GOOGLE_SEARCH_URL.replace('◳t', re.sub(r'[\/\'".,&*@!#()\[\]\{\}]', '', term)) return self.image_search(query) def decodeURL(self, url1, url2, protocol, audiohost, server): url2 = protocol + "//" + server + "/player-mp3-highHandler.php?path=" + url2; url1 = protocol + "//" + audiohost + "/mp3/" + base64.b64decode(url1).decode("utf-8", "strict") return url1, url2 def attemptFetchForvoLinks(self,term): urls = self.search(term) if len(urls) > 0: return json.dumps(urls) else: return False def generateURLS(self, results): audio = re.findall(r'var pronunciations = \[([\w\W\n]*?)\];', results) if not audio: return [] audio = audio[0] data = re.findall(self.selLang + r'.*?Pronunciation by (?:<a.*?>)?(\w+).*?class="lang_xx"\>(.*?)\<.*?,.*?,.*?,.*?,\'(.+?)\',.*?,.*?,.*?\'(.+?)\'', audio) if data: server = re.search(r"var _SERVER_HOST=\'(.+?)\';", results).group(1) audiohost = re.search(r'var _AUDIO_HTTP_HOST=\'(.+?)\';', results).group(1) protocol = 'https:' urls = [] for datum in data: url1, url2 = self.decodeURL(datum[2],datum[3],protocol, audiohost, server) urls.append([datum[0],datum[1], url1, url2]) return urls else: return [] def setSearchRegion(self, region): self.region = region def image_search(self, query_gen): try: html = self.session.get(query_gen).text except: showInfo('The Forvo Dictionary could not be loaded, please confirm that your are connected to the internet and try again. ') return [] results = html return self.generateURLS(results)
class LineEditWithFocusedSignal(QLineEdit): focused = pyqtSignal() def focusInEvent(self, e): self.focused.emit()
class ProfileSettingsDialog(QDialog): loginPageIndex = 0 logoutPageIndex = 1 logged_in = pyqtSignal(dict) unauthorized_login = pyqtSignal(dict) logged_out = pyqtSignal() logout_error = pyqtSignal(dict) token_invalidated = pyqtSignal(dict) connection_error = pyqtSignal() def __init__(self, parent, network_thread, user_repo, user_is_logged_in, achievements_repo): super().__init__(parent) self.ui = Ui_ProfileSettingsDialog() self.ui.setupUi(self) self._network_thread = network_thread self._user_repo = user_repo self._achievements_repo = achievements_repo self._connect_login_signals() self._connect_logout_signals() self._connect_token_validation_signals() self._connect_connection_error_signal() self._connect_login_button() self._connect_logout_button() self._connect_signup_button() self._show_correct_auth_form(user_is_logged_in) self._validate_token_if_logged_in(user_is_logged_in) def keyPressEvent(self, event) -> None: key = event.key() if key == Qt.Key_Return or key == Qt.Key_Enter: event.ignore() else: super().keyPressEvent(event) def _connect_login_signals(self): self.logged_in.connect(self.on_successful_login) self.logged_in.connect(self._start_sync_job) self.unauthorized_login.connect(self.on_unauthorized) def _connect_logout_signals(self): self.logged_out.connect(self.on_logout) self.logout_error.connect(self.on_logout_error) def _connect_token_validation_signals(self): self.token_invalidated.connect(self.on_token_invalidated) def _connect_connection_error_signal(self): self.connection_error.connect(self.on_connection_error) def _connect_login_button(self): self.ui.loginButton.clicked.connect(self._login) def _login(self): email = self.ui.emailLineEdit.text() password = self.ui.passwordLineEdit.text() login_job = partial( accounts.login, email, password, listener=self, user_repo=self._user_repo, ) self._network_thread.put(login_job) def on_successful_login(self, user_attrs): self._switchToLogoutPage(user_attrs) self._clear_login_form() def _switchToLogoutPage(self, user_attrs): self.ui.userEmailLabel.setText(user_attrs["uid"]) self.ui.stackedWidget.setCurrentIndex(self.logoutPageIndex) def _clear_login_form(self): email = self.ui.emailLineEdit.setText("") password = self.ui.passwordLineEdit.setText("") def _start_sync_job(self): leaderboards.sync_if_logged_in( user_repo=self._user_repo, achievements_repo=self._achievements_repo, network_thread=self._network_thread, http_client=TokenAuthHttpClient(self._user_repo), ) def on_unauthorized(self, response): self.ui.statusLabel.setText(response["errors"][0]) def on_connection_error(self): self.ui.statusLabel.setText("Error connecting to server. Ensure you are connected to the internet.\nIf you are and the error persists, the server may be down. Try again later.") def _connect_logout_button(self): self.ui.logoutButton.clicked.connect(self._logout) def _logout(self): logout_job = partial( accounts.logout, self._user_repo, listener=self ) self._network_thread.put(logout_job) def on_logout(self): self.ui.statusLabel.setText("User logged out successfully.") self._switchToLoginPage() def on_logout_error(self, response): self.ui.statusLabel.setText(response["errors"][0]) self._switchToLoginPage() def _switchToLoginPage(self): self.ui.stackedWidget.setCurrentIndex(self.loginPageIndex) def _connect_signup_button(self): signup_url = urljoin(sra_base_url, "users/sign_up") self.ui.signupLabel.linkActivated.connect(lambda: webbrowser.open(signup_url)) def _show_correct_auth_form(self, user_is_logged_in): if user_is_logged_in: user = self._user_repo.load() self._switchToLogoutPage(user_attrs=attr.asdict(user)) else: self._switchToLoginPage() def _validate_token_if_logged_in(self, user_is_logged_in): if user_is_logged_in: job = partial( accounts.validate_token, self._user_repo, listener=self, ) self._network_thread.put(job) def on_token_invalidated(self, response): self.ui.statusLabel.setText(response["errors"][0]) self._switchToLoginPage()
class AnkiApp(QApplication): # Single instance support on Win32/Linux ################################################## appMsg = pyqtSignal(str) KEY = "anki" + checksum(getpass.getuser()) TMOUT = 30000 def __init__(self, argv): QApplication.__init__(self, argv) self._argv = argv def secondInstance(self): # we accept only one command line argument. if it's missing, send # a blank screen to just raise the existing window opts, args = parseArgs(self._argv) buf = "raise" if args and args[0]: buf = os.path.abspath(args[0]) if self.sendMsg(buf): print("Already running; reusing existing instance.") return True else: # send failed, so we're the first instance or the # previous instance died QLocalServer.removeServer(self.KEY) self._srv = QLocalServer(self) self._srv.newConnection.connect(self.onRecv) self._srv.listen(self.KEY) return False def sendMsg(self, txt): sock = QLocalSocket(self) sock.connectToServer(self.KEY, QIODevice.WriteOnly) if not sock.waitForConnected(self.TMOUT): # first instance or previous instance dead return False sock.write(txt.encode("utf8")) if not sock.waitForBytesWritten(self.TMOUT): # existing instance running but hung QMessageBox.warning( None, "Anki Already Running", "If the existing instance of Anki is not responding, please close it using your task manager, or restart your computer." ) sys.exit(1) sock.disconnectFromServer() return True def onRecv(self): sock = self._srv.nextPendingConnection() if not sock.waitForReadyRead(self.TMOUT): sys.stderr.write(sock.errorString()) return path = bytes(sock.readAll()).decode("utf8") self.appMsg.emit(path) sock.disconnectFromServer() # OS X file/url handler ################################################## def event(self, evt): if evt.type() == QEvent.FileOpen: self.appMsg.emit(evt.file() or "raise") return True return QApplication.event(self, evt)
class Notification(QLabel): # Anki dialog manager support silentlyClose = True closed = pyqtSignal() def __init__( self, text: str, settings: NotificationSettings = NotificationSettings(), parent: Optional[QWidget] = None, **kwargs, ): super().__init__(text, parent=parent, **kwargs) self._settings = settings self.setFrameStyle(QFrame.Shape.Panel) self.setLineWidth(2) self.setWindowFlags(Qt.WindowType.ToolTip) self.setContentsMargins(10, 10, 10, 10) palette = QPalette() palette.setColor(QPalette.ColorRole.Window, QColor(self._settings.bg_color)) palette.setColor(QPalette.ColorRole.WindowText, QColor(self._settings.fg_color)) self.setPalette(palette) if parent and self._settings.focus_behavior != FocusBehavior.always_on_top: app: "AnkiApp" = QApplication.instance( ) # type: ignore[assignment] app.focusChanged.connect(self._on_app_focus_changed) def _on_app_focus_changed(self, old_widget: Optional[QWidget], new_widget: Optional[QWidget]): focus_behavior = self._settings.focus_behavior focus_exceptions = self._settings.focus_behavior_exceptions parent_window = self.parent().window() old_window = old_widget.window() if old_widget else None new_window = new_widget.window() if new_widget else None if focus_exceptions and any( isinstance(old_window, wtype) for wtype in focus_exceptions): # switching back from an excluded window should not cause notif closing pass elif new_window is None and QApplication.widgetAt( QCursor.pos()) == self: # clicking on self should not dismiss notification when not configured as # such (Windows bug) pass elif new_window is None: # switched focus away from application self.close() elif new_window != parent_window and (not focus_exceptions or (all( not isinstance(new_window, wtype) for wtype in focus_exceptions))): # switched to other window within same application that's not excluded if focus_behavior == FocusBehavior.close_on_window_focus_lost: self.close() elif focus_behavior == FocusBehavior.lower_on_window_focus_lost: self.setWindowFlag(Qt.WindowType.ToolTip, on=False) elif (new_window == parent_window and focus_behavior == FocusBehavior.lower_on_window_focus_lost): self.setWindowFlag(Qt.WindowType.ToolTip, on=True) self.show() def mousePressEvent(self, event: QMouseEvent): if (not self._settings.dismiss_on_click or self.cursor().shape() == Qt.CursorShape.PointingHandCursor): # Do not ignore mouse press event if configured that way and/or # currently hovering link (as signaled by cursor shape) return super().mousePressEvent(event) event.accept() self.close() def closeEvent(self, event: QCloseEvent): self.closed.emit() return super().closeEvent(event) def resizeEvent(self, event: QResizeEvent) -> None: # true geometry is only known once resizeEvent fires self.update_position() super().resizeEvent(event) def update_position(self): align_horizontal = self._settings.align_horizontal align_vertical = self._settings.align_vertical if align_horizontal == NotificationHAlignment.left: x = 0 + self._settings.space_horizontal elif align_horizontal == NotificationHAlignment.right: x = self.parent().width() - self.width( ) - self._settings.space_horizontal elif align_horizontal == NotificationHAlignment.center: x = (self.parent().width() - self.width()) / 2 else: raise ValueError( f"Alignment value {align_horizontal} is not supported") if align_vertical == NotificationVAlignment.top: y = 0 + self._settings.space_vertical elif align_vertical == NotificationVAlignment.bottom: y = self.parent().height() - self.height( ) - self._settings.space_vertical elif align_vertical == NotificationVAlignment.center: y = (self.parent().height() - self.height()) / 2 else: raise ValueError( f"Alignment value {align_vertical} is not supported") self.move(self.parent().mapToGlobal(QPoint(x, y)) # type:ignore )
class TagEdit(QLineEdit): lostFocus = pyqtSignal() # 0 = tags, 1 = decks def __init__(self, parent, type=0): QLineEdit.__init__(self, parent) self.col = None self.model = QStringListModel() self.type = type if type == 0: self.completer = TagCompleter(self.model, parent, self) else: self.completer = QCompleter(self.model, parent) self.completer.setCompletionMode(QCompleter.PopupCompletion) self.completer.setCaseSensitivity(Qt.CaseInsensitive) self.setCompleter(self.completer) def setCol(self, col): "Set the current col, updating list of available tags." self.col = col if self.type == 0: l = sorted(self.col.tags.all()) else: l = sorted(self.col.decks.allNames()) self.model.setStringList(l) def focusInEvent(self, evt): QLineEdit.focusInEvent(self, evt) def keyPressEvent(self, evt): if evt.key() in (Qt.Key_Up, Qt.Key_Down): # show completer on arrow key up/down if not self.completer.popup().isVisible(): self.showCompleter() return if (evt.key() == Qt.Key_Tab and evt.modifiers() & Qt.ControlModifier): # select next completion if not self.completer.popup().isVisible(): self.showCompleter() index = self.completer.currentIndex() self.completer.popup().setCurrentIndex(index) cur_row = index.row() if not self.completer.setCurrentRow(cur_row + 1): self.completer.setCurrentRow(0) return if evt.key() in (Qt.Key_Enter, Qt.Key_Return): # apply first completion if no suggestion selected selected_row = self.completer.popup().currentIndex().row() if selected_row == -1: self.completer.setCurrentRow(0) index = self.completer.currentIndex() self.completer.popup().setCurrentIndex(index) self.hideCompleter() QWidget.keyPressEvent(self, evt) return QLineEdit.keyPressEvent(self, evt) if not evt.text(): # if it's a modifier, don't show return if evt.key() not in ( Qt.Key_Enter, Qt.Key_Return, Qt.Key_Escape, Qt.Key_Space, Qt.Key_Tab, Qt.Key_Backspace, Qt.Key_Delete): self.showCompleter() def showCompleter(self): self.completer.setCompletionPrefix(self.text()) self.completer.complete() def focusOutEvent(self, evt): QLineEdit.focusOutEvent(self, evt) self.lostFocus.emit() self.completer.popup().hide() def hideCompleter(self): if sip.isdeleted(self.completer): return self.completer.popup().hide()
class DockableWithClose(QDockWidget): closed = pyqtSignal() def closeEvent(self, evt): self.closed.emit() QDockWidget.closeEvent(self, evt)
class ErrorHandler(QObject): "Catch stderr and write into buffer." ivl = 100 errorTimer = pyqtSignal() def __init__(self, mw): QObject.__init__(self, mw) self.mw = mw self.timer = None self.errorTimer.connect(self._setTimer) self.pool = "" self._oldstderr = sys.stderr sys.stderr = self def unload(self): sys.stderr = self._oldstderr sys.excepthook = None def write(self, data): # dump to stdout sys.stdout.write(data) # save in buffer self.pool += data # and update timer self.setTimer() def setTimer(self): # we can't create a timer from a different thread, so we post a # message to the object on the main thread self.errorTimer.emit() def _setTimer(self): if not self.timer: self.timer = QTimer(self.mw) self.timer.timeout.connect(self.onTimeout) self.timer.setInterval(self.ivl) self.timer.setSingleShot(True) self.timer.start() def tempFolderMsg(self): return _("""Unable to access Anki media folder. The permissions on \ your system's temporary folder may be incorrect.""") def onTimeout(self): error = html.escape(self.pool) self.pool = "" self.mw.progress.clear() if "abortSchemaMod" in error: return if "10013" in error: return showWarning( _("Your firewall or antivirus program is preventing Anki from creating a connection to itself. Please add an exception for Anki." )) if "Pyaudio not" in error: return showWarning(_("Please install PyAudio")) if "install mplayer" in error: return showWarning( _("Sound and video on cards will not function until mpv or mplayer is installed." )) if "no default input" in error.lower(): return showWarning( _("Please connect a microphone, and ensure " "other programs are not using the audio device.")) if "invalidTempFolder" in error: return showWarning(self.tempFolderMsg()) if "Beautiful Soup is not an HTTP client" in error: return if "database or disk is full" in error: return showWarning( _("Your computer's storage may be full. Please delete some unneeded files, then try again." )) if "disk I/O error" in error: return showWarning( _("""\ An error occurred while accessing the database. Possible causes: - Antivirus, firewall, backup, or synchronization software may be \ interfering with Anki. Try disabling such software and see if the \ problem goes away. - Your disk may be full. - The Documents/Anki folder may be on a network drive. - Files in the Documents/Anki folder may not be writeable. - Your hard disk may have errors. It's a good idea to run Tools>Check Database to ensure your collection \ is not corrupt. """)) stdText = _("""\ <h1>Error</h1> <p>An error occurred. Please use <b>Tools > Check Database</b> to see if \ that fixes the problem.</p> <p>If problems persist, please report the problem on our \ <a href="https://help.ankiweb.net">support site</a>. Please copy and paste \ the information below into your report.</p>""") pluginText = _("""\ <h1>Error</h1> <p>An error occurred. Please start Anki while holding down the shift \ key, which will temporarily disable the add-ons you have installed.</p> <p>If the issue only occurs when add-ons are enabled, please use the \ Tools>Add-ons menu item to disable some add-ons and restart Anki, \ repeating until you discover the add-on that is causing the problem.</p> <p>When you've discovered the add-on that is causing the problem, please \ report the issue on the <a href="https://help.ankiweb.net/discussions/add-ons/">\ add-ons section</a> of our support site. <p>Debug info:</p> """) if self.mw.addonManager.dirty: txt = pluginText error = supportText() + self._addonText(error) + "\n" + error else: txt = stdText error = supportText() + "\n" + error # show dialog txt = txt + "<div style='white-space: pre-wrap'>" + error + "</div>" showText(txt, type="html", copyBtn=True) def _addonText(self, error): matches = re.findall(r"addons21/(.*?)/", error) if not matches: return "" # reverse to list most likely suspect first, dict to deduplicate: addons = [ aqt_mw.addonManager.addonName(i) for i in dict.fromkeys(reversed(matches)) ] txt = _("""Add-ons possibly involved: {}\n""") # highlight importance of first add-on: addons[0] = "<b>{}</b>".format(addons[0]) return txt.format(", ".join(addons))
class Google(QThread): resultsFound = pyqtSignal(list) noResults = pyqtSignal(str) def __init__(self): QThread.__init__(self) self.GOOGLE_SEARCH_URL = "https://www.google.com/search" self.term = False self.initSession() def initSession(self): self.session = requests.session() self.session.headers.update({ "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:10.0) \ Gecko/20100101 Firefox/10.0" }) def setTermIdName(self, term, idName): self.term = term self.idName = idName def run(self): if self.term: resultList = self.getPreparedResults(self.term, self.idName) self.resultsFound.emit(resultList) def search(self, keyword, maximum, region=False): query = self.query_gen(keyword) return self.image_search(query, maximum, region) def query_gen(self, keyword): page = 0 while True: params = urllib.parse.urlencode({"q": keyword, "tbm": "isch"}) if self.region == 'Japan': url = 'https://www.google.co.jp/search' else: url = self.GOOGLE_SEARCH_URL yield url + "?" + params page += 1 def setSearchRegion(self, region): self.region = region def getResultsFromRawHtml(self, html): pattern = r"AF_initDataCallback[\s\S]+AF_initDataCallback\({key: '[\s\S]+?',[\s\S]+?return (\[[\s\S]+\])[\s\S]+?<\/body><\/html>" matches = re.findall(pattern, html) results = [] try: if len(matches) > 0: decoded = json.loads(matches[0])[31][0][12][2] for d in decoded: d1 = d[1] if d1: results.append(str(d1[3][0])) return results except: return [] def getHtml(self, term): images = self.search(term, 80) if not images or len(images) < 1: return 'No Images Found. This is likely due to a connectivity error.' firstImages = [] tempImages = [] for idx, image in enumerate(images): tempImages.append(image) if len(tempImages) > 2 and len(firstImages) < 1: firstImages += tempImages tempImages = [] if len(tempImages) > 2 and len(firstImages) > 1: break html = '<div class="googleCont">' for img in firstImages: html += '<div class="imgBox"><div onclick="toggleImageSelect(this)" data-url="' + img + '" class="googleHighlight"></div><img class="googleImage" src="' + img + '"></div>' html += '</div><div class="googleCont">' for img in tempImages: html += '<div class="imgBox"><div onclick="toggleImageSelect(this)" data-url="' + img + '" class="googleHighlight"></div><img class="googleImage" src="' + img + '"></div>' html += '</div><button class="imageLoader" onclick="loadMoreImages(this, \\\'' + '\\\' , \\\''.join( self.getCleanedUrls(images)) + '\\\')">Load More</button>' return html def getPreparedResults(self, term, idName): html = self.getHtml(term) return [html, idName] def getCleanedUrls(self, urls): return [x.replace('\\', '\\\\') for x in urls] def image_search(self, query_gen, maximum, region=False): results = [] if not region: region = countryCodes[self.region] total = 0 finished = False while True: try: count = 0 while not finished: count += 1 hr = self.session.get( next(query_gen) + '&ijn=0&cr=' + region) html = hr.text if not html and not '<!doctype html>' in html: if count > 5: finished = True break self.initSession() time.sleep(.1) else: finished = True break except: self.noResults.emit( 'The Google Image Dictionary could not establish a connection. Please ensure you are connected to the internet and try again. If you will be without internet for some time, consider using a template that does not include the Google Images Dictionary in order to prevent this message appearing everytime a search is performed. ' ) return False results = self.getResultsFromRawHtml(html) if len(results) == 0: soup = BeautifulSoup(html, "html.parser") elements = soup.select(".rg_meta.notranslate") jsons = [json.loads(e.get_text()) for e in elements] image_url_list = [js["ou"] for js in jsons] if not len(image_url_list): break elif len(image_url_list) > maximum - total: results += image_url_list[:maximum - total] break else: results += image_url_list total += len(image_url_list) else: break return results