def loadCollection(self): cpath = self.pm.collectionPath() try: self.col = Collection(cpath, log=True) except anki.db.Error: # warn user showWarning(_("""\ Your collection is corrupt. Please create a new profile, then \ see the manual for how to restore from an automatic backup. Debug info: """)+traceback.format_exc()) self.unloadProfile() except Exception as 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.progress.setupDB(self.col.db) self.maybeEnableUndo() self.moveToState("deckBrowser")
def onTimeout(self): error = self.pool self.pool = "" self.mw.progress.clear() if "abortSchemaMod" in error: return if "Pyaudio not" in error: return showWarning(_("Please install PyAudio")) if "install mplayer" in error: return showWarning(_("Please install mplayer")) if "no default output" in error: return showWarning(_("Please connect a microphone.")) stdText = _("""\ An error occurred. It may have been caused by a harmless bug, <br> or your deck may have a problem. <p>To confirm it's not a problem with your deck, please run <b>Tools > Maintenance > Check Database</b>. <p>If that doesn't fix the problem, please copy the following<br> into a bug report:""") pluginText = _("""\ An error occurred in an add-on. Please contact the add-on author.<br>""") if "addon" in error: txt = pluginText else: txt = stdText # show dialog txt = txt + "<div style='white-space: pre-wrap'>" + error + "</div>" showText(txt, type="html")
def accept(self): QDialog.accept(self) # get codes try: ids = [int(n) for n in self.form.code.text().split()] except ValueError: showWarning(_("Invalid code.")) return errors = [] self.mw.progress.start(immediate=True) for n in ids: ret = download(self.mw, n) if ret[0] == "error": errors.append(_("Error downloading %(id)s: %(error)s") % dict(id=n, error=ret[1])) continue data, fname = ret self.mw.addonManager.install(data, fname) self.mw.progress.finish() if not errors: tooltip(_("Download successful. Please restart Anki."), period=3000) else: showWarning("\n".join(errors))
def __init__(self, mw): self.mw = mw f = self.mw.form; s = SIGNAL("triggered()") self.mw.connect(f.actionOpenPluginFolder, s, self.onOpenPluginFolder) self.mw.connect(f.actionEnableAllPlugins, s, self.onEnableAllPlugins) self.mw.connect(f.actionDisableAllPlugins, s, self.onDisableAllPlugins) if isWin: self.clearPluginCache() self.disableObsoletePlugins() plugdir = self.pluginsFolder() sys.path.insert(0, plugdir) plugins = self.enabledPlugins() plugins.sort() self.registeredPlugins = {} for plugin in plugins: try: nopy = plugin.replace(".py", "") __import__(nopy) except: print "Error in %s" % plugin traceback.print_exc() self.mw.disableCardMenuItems() self.rebuildPluginsMenu() # run the obsolete init hook try: runHook('init') except: showWarning( _("Broken plugin:\n\n%s") % unicode(traceback.format_exc(), "utf-8", "replace"))
def onChangeDeck(self): # based on aqt/browser.py:setDeck(), and add-on writing guide example self.mw.checkpoint(_("Suspend")) from aqt.studydeck import StudyDeck cids = [self.card.id] did = self.mw.col.db.scalar( "select did from cards where id = ?", cids[0]) current = self.mw.col.decks.get(did)['name'] ret = StudyDeck( self.mw, current=current, accept=_("Move Cards"), title = _("Change Deck"), help="browse", parent=self) if not ret.name: return did = self.mw.col.decks.id(ret.name) deck = self.mw.col.decks.get(did) if deck['dyn']: showWarning(_("Cards can't be manually moved into a filtered deck.")) return self.mw.checkpoint(_("Change deck")) self.card.did = did # if the card was in a cram deck, we have to put back the original due # time and original deck self.card.odid = 0 if self.card.odue: self.card.due = self.card.odue self.card.odue = 0 self.accept()
def onTimeout(self): error = cgi.escape(self.pool) self.pool = "" self.mw.progress.clear() if "abortSchemaMod" in error: return if "Pyaudio not" in error: return showWarning(_("Please install PyAudio")) if "install mplayer" in error: return showWarning(_("Please install mplayer")) if "no default output" in error: return showWarning(_("Please connect a microphone, and ensure " "other programs are not using the audio device.")) stdText = _("""\ An error occurred. It may have been caused by a harmless bug, <br> or your deck may have a problem. <p>To confirm it's not a problem with your deck, please run <b>Tools > Check Database</b>. <p>If that doesn't fix the problem, please copy the following<br> into a bug report:""") pluginText = _("""\ An error occurred in an add-on.<br> Please post on the add-on forum:<br>%s<br>""") pluginText %= "https://groups.google.com/forum/#!forum/anki-addons" if "addon" in error: txt = pluginText else: txt = stdText # show dialog txt = txt + "<div style='white-space: pre-wrap'>" + error + "</div>" showText(txt, type="html")
def accept(self): file = getSaveFile( self, _("Export"), "export", self.exporter.key, self.exporter.ext) self.hide() if file: self.exporter.includeSched = ( self.frm.includeSched.isChecked()) self.exporter.includeMedia = ( self.frm.includeMedia.isChecked()) self.exporter.includeTags = ( self.frm.includeTags.isChecked()) if not self.frm.deck.currentIndex(): self.exporter.did = None else: name = self.decks[self.frm.deck.currentIndex()] self.exporter.did = self.col.decks.id(name) self.mw.progress.start(immediate=True) try: f = open(file, "wb") f.close() except (OSError, IOError), e: showWarning(_("Couldn't save file: %s") % unicode(e)) else: os.unlink(file) self.exporter.exportInto(file) tooltip(_("%d exported.") % self.exporter.count) finally:
def onExportList(browser): # big reach! if not browser.form.tableView.selectionModel().hasSelection(): showWarning("Please select 1 or more rows", browser, help="") return path = getSaveFile(browser, _("Export Browser List"), "exportcsv", _("Cards as CSV"), '.csv', 'exportcsv.csv') if not path: return file = open(path, "wb") writer = csv.writer(file, dialect='excel') for rowIndex in browser.form.tableView.selectionModel().selectedRows(): #wasn't sure if I could modify the "Index" objects we get back from # the selection model. Since the columnData function only accesses # the row() and column() functions, seemed safer to create new # instances. row = rowIndex.row() rowdata = [] for column in range(browser.model.columnCount(0)): index = RowAndColumn(row, column) #let the browser model's columnData function do all the hard work answer = escapeText(browser.model.columnData(index)) rowdata.append(answer.encode('utf8')) writer.writerow(rowdata) file.close()
def readValues(self): mw.EXPORT.FORMAT_INDEX = self.ui.formatCombo.currentIndex() delimiter = self.ui.csvEdit.text() if mw.EXPORT.FORMAT == "CSV" and mw.EXPORT.CSV_DELIMITER == "": showWarning(_("The CSV delimiter is not set")) return False if delimiter == "\\t": mw.EXPORT.CSV_DELIMITER = "\t" else: mw.EXPORT.CSV_DELIMITER = delimiter mw.EXPORT.FILEPATH = self.ui.fileEdit.text() if mw.EXPORT.FILEPATH == "": showWarning(_("The file name is not set")) return False mw.EXPORT.FILTER_DECK_INDEX = self.ui.deckFilterCombo.currentIndex() mw.EXPORT.FILTER_TAGS = self.ui.tagFilterEdit.text().strip() mw.EXPORT.ALL_FIELDS = self.ui.allFieldsCheck.isChecked() mw.EXPORT.REVERSE_CARDS = self.ui.reverseCardsCheck.isChecked() mw.EXPORT.TAGS = self.ui.allTagsCheck.isChecked() mw.EXPORT.DECK_NAME = self.ui.deckNameCheck.isChecked() mw.EXPORT.CARD_ID = self.ui.cardIdCheck.isChecked() mw.EXPORT.NOTE_ID = self.ui.noteIdCheck.isChecked() mw.EXPORT.CARD_TYPE = self.ui.cardTypeCheck.isChecked() mw.EXPORT.NOTE_TYPE = self.ui.noteTypeCheck.isChecked() return True
def onOpenProfile(self): if not self.openProfile(): showWarning(_("Invalid password.")) return self.profileDiag.close() self.loadProfile() return True
def accept(self): self.exporter.includeSched = ( self.frm.includeSched.isChecked()) self.exporter.includeMedia = ( self.frm.includeMedia.isChecked()) self.exporter.includeTags = ( self.frm.includeTags.isChecked()) if not self.frm.deck.currentIndex(): self.exporter.did = None else: name = self.decks[self.frm.deck.currentIndex()] self.exporter.did = self.col.decks.id(name) if (self.isApkg and self.exporter.includeSched and not self.exporter.did): verbatim = True # it's a verbatim apkg export, so place on desktop instead of # choosing file file = os.path.join(QDesktopServices.storageLocation( QDesktopServices.DesktopLocation), "collection.apkg") if os.path.exists(file): if not askUser( _("%s already exists on your desktop. Overwrite it?")% "collection.apkg"): return else: verbatim = False # Get deck name and remove invalid filename characters deck_name = self.decks[self.frm.deck.currentIndex()] deck_name = re.sub('[\\\\/?<>:*|"^]', '_', deck_name) filename = os.path.join(aqt.mw.pm.base, u'{0}{1}'.format(deck_name, self.exporter.ext)) while 1: file = getSaveFile(self, _("Export"), "export", self.exporter.key, self.exporter.ext, fname=filename) if not file: return if checkInvalidFilename(os.path.basename(file), dirsep=False): continue break self.hide() if file: self.mw.progress.start(immediate=True) try: f = open(file, "wb") f.close() except (OSError, IOError), e: showWarning(_("Couldn't save file: %s") % unicode(e)) else: os.unlink(file) self.exporter.exportInto(file) if verbatim: msg = _("A file called collection.apkg was saved on your desktop.") period = 5000 else: period = 3000 msg = ngettext("%d card exported.", "%d cards exported.", \ self.exporter.count) % self.exporter.count tooltip(msg, period=period) finally:
def replaceWithApkg(mw, file, backup): # unload collection, which will also trigger a backup mw.unloadCollection() # overwrite collection z = zipfile.ZipFile(file) try: z.extract("collection.anki2", mw.pm.profileFolder()) except: showWarning(_("The provided file is not a valid .apkg file.")) return # because users don't have a backup of media, it's safer to import new # data and rely on them running a media db check to get rid of any # unwanted media. in the future we might also want to deduplicate this # step d = os.path.join(mw.pm.profileFolder(), "collection.media") for n, (cStr, file) in enumerate( json.loads(z.read("media").decode("utf8")).items()): mw.progress.update(ngettext("Processed %d media file", "Processed %d media files", n) % n) size = z.getinfo(cStr).file_size dest = os.path.join(d, file) # if we have a matching file size if os.path.exists(dest) and size == os.stat(dest).st_size: continue data = z.read(cStr) open(dest, "wb").write(data) z.close() # reload mw.loadCollection() if backup: mw.col.modSchema(check=False) mw.progress.finish()
def _copySettings(self): p = self.mw.pm.profile for k in ( "recentColours", "stripHTML", "editFontFamily", "editFontSize", "editLineSize", "deleteMedia", "preserveKeyboard", "numBackups", "proxyHost", "proxyPass", "proxyPort", "proxyUser", ): try: p[k] = self.conf[k] except: showWarning( _( """\ Anki 2.0 only supports automatic upgrading from Anki 1.2. To load old \ decks, please open them in Anki 1.2 to upgrade them, and then import them \ into Anki 2.0.""" ) ) return return True
def addNote(self, note): note.model()["did"] = self.deckChooser.selectedId() ret = note.dupeOrEmpty() if ret == 1: showWarning(_("The first field is empty."), help="AddItems#AddError") return if "{{cloze:" in note.model()["tmpls"][0]["qfmt"]: if not self.mw.col.models._availClozeOrds(note.model(), note.joinedFields(), False): if not askUser( _("You have a cloze deletion note type " "but have not made any cloze deletions. Proceed?") ): return cards = self.mw.col.addNote(note) if not cards: showWarning( _( """\ The input you have provided would make an empty \ question on all cards.""" ), help="AddItems", ) return self.addHistory(note) self.mw.requireReset() return note
def nm_on(): """ Turn on night mode. """ if not nm_profile_loaded: showWarning(NM_ERROR_NO_PROFILE) return False global nm_state_on try: nm_state_on = True import aqt.browser aqt.browser.COLOUR_MARKED = "#735083" aqt.browser.COLOUR_SUSPENDED = "#777750" nm_append_to_styles(nm_css_bottom, nm_css_body + nm_card_color_css() + nm_css_custom_color_map(), nm_css_top, nm_css_decks + nm_body_color_css(), nm_css_other_bottoms, nm_css_overview() + nm_body_color_css(), nm_css_menu, nm_css_buttons + nm_body_color_css()) nm_menu_switch.setChecked(True) return True except: showWarning(NM_ERROR_SWITCH) return False
def download(mw, code): "Download addon/deck from AnkiWeb. On success caller must stop progress diag." # check code is valid try: code = int(code) except ValueError: showWarning(_("Invalid code.")) return # create downloading thread thread = Downloader(code) def onRecv(): try: mw.progress.update(label="%dKB downloaded" % (thread.recvTotal/1024)) except NameError: # some users report the following error on long downloads # NameError: free variable 'mw' referenced before assignment in enclosing scope # unsure why this is happening, but guard against throwing the # error pass thread.recv.connect(onRecv) thread.start() mw.progress.start(immediate=True) while not thread.isFinished(): mw.app.processEvents() thread.wait(100) if not thread.error: # success return thread.data, thread.fname else: mw.progress.finish() showWarning(_("Download failed: %s") % thread.error)
def unloadCollection(self): """ Unload the collection. This unloads a collection if there is one and returns True if there is no collection after the call. (Because the unload worked or because there was no collection to start with.) """ if self.col: if not self.closeAllCollectionWindows(): return self.progress.start(immediate=True) corrupt = False try: self.maybeOptimize() except: corrupt = True if not corrupt: 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 \ when the collection is stored on a network or cloud drive. Please see \ the manual for information on how to restore from an automatic backup.")) self.col.close() self.col = None if not corrupt: self.backup() self.progress.finish() return True
def downloadIds(self, ids): log = [] errs = [] self.mw.progress.start(immediate=True) for n in ids: ret = download(self.mw, n) if ret[0] == "error": errs.append(_("Error downloading %(id)s: %(error)s") % dict(id=n, error=ret[1])) continue data, fname = ret fname = fname.replace("_", " ") name = os.path.splitext(fname)[0] ret = self.install(io.BytesIO(data), manifest={"package": str(n), "name": name, "mod": intTime()}) if ret[0] is False: if ret[1] == "conflicts": continue if ret[1] == "zip": showWarning(_("The download was corrupt. Please try again.")) elif ret[1] == "manifest": showWarning(_("Invalid add-on manifest.")) log.append(_("Downloaded %(fname)s" % dict(fname=name))) self.mw.progress.finish() return log, errs
def _unloadCollection(self): if not self.col: return if self.restoringBackup: label = _("Closing...") else: label = _("Backing Up...") self.progress.start(label=label, immediate=True) corrupt = False try: self.maybeOptimize() if not devMode: corrupt = self.col.db.scalar("pragma integrity_check") != "ok" except: corrupt = True try: self.col.close() except: corrupt = True finally: self.col = None 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 \ when the collection is stored on a network or cloud drive. If problems \ persist after restarting your computer, please open an automatic backup \ from the profile screen.")) if not corrupt and not self.restoringBackup: self.backup() self.progress.finish()
def install(self, sid, data, fname): try: z = ZipFile(io.BytesIO(data)) except zipfile.BadZipfile: showWarning(_("The download was corrupt. Please try again.")) return name = os.path.splitext(fname)[0] # previously installed? meta = self.addonMeta(sid) base = self.addonsFolder(sid) if os.path.exists(base): self.deleteAddon(sid) # extract os.mkdir(base) for n in z.namelist(): if n.endswith("/"): # folder; ignore continue # write z.extract(n, base) # update metadata meta['name'] = name meta['mod'] = intTime() self.writeAddonMeta(sid, meta)
def install(self, sid, data, fname): try: z = ZipFile(io.BytesIO(data)) except zipfile.BadZipfile: showWarning(_("The download was corrupt. Please try again.")) return name = os.path.splitext(fname)[0] # previously installed? meta = self.addonMeta(sid) base = self.addonsFolder(sid) if os.path.exists(base): self.backupUserFiles(sid) self.deleteAddon(sid) os.mkdir(base) self.restoreUserFiles(sid) # extract for n in z.namelist(): if n.endswith("/"): # folder; ignore continue path = os.path.join(base, n) # skip existing user files if os.path.exists(path) and n.startswith("user_files/"): continue z.extract(n, base) # update metadata meta['name'] = name meta['mod'] = intTime() self.writeAddonMeta(sid, meta)
def identifyNotes(card_ids): mw.progress.start(label="Reset Creation Times: collecting notes...", max=len(card_ids)) last_nid = "" nids_ordered = [] nids_lookup = {} # debug # showInfo(("Called identifyNotes with %s cards") % len(card_ids)) # Loop through the selected cards, detecting unique notes and saving the # note IDs for driving the DB update. card_cnt = 0 for card_cnt, card_id in enumerate(card_ids): # debug # showInfo(("Loop: Processing card id %s") % card_id) mw.progress.update(label=("Reset Creation Times: collecting note for card %s") % card_id, value=card_cnt) # Retrieve the selected card to get the note ID. card = mw.col.getCard(card_id) # debug #showInfo(("retrieved card_id %s with note_id %s") % (card_id, card.nid)) # We expect sibling cards (of a note) to be grouped together. When a new # note is encountered, save it for later processing. if card.nid != last_nid: # I don't think this could ever happen: # This is a precaution that a note's sibling cards are grouped # together. If it were possible for them to be sorted in the browser # in a way that they wouldn't be contiguous, this would cause the # underlying note (creation time) to be processed twice in a way the # user didn't intend. Anki's data model makes this logically # possible, but the browser may prevent it. This test is a way to be # absolutely certain. if card.nid in nids_lookup: showWarning("A note found out of order. Your cards appear to be sorted in a way that siblings are not contiguous. It is not possible to reset the create time this way. Please report this to the addon discussion forum.") # Add the nid to the end of an array which will be used to drive the # DB update. This maintains note ids in the same order which the # cards appear in the browser. nids_ordered.append(card.nid) # Add the nid to a dictionary so we can easily reference it (to see # if a nid was previously encountered. I.e., whether sibling cards # weren't grouped together in the browser.). nids_lookup.update({card.nid:1}) # Save the new nid value so we can skip sibling cards and detect # when a card belonging to a different note is encountered. last_nid = card.nid mw.progress.finish() return card_cnt + 1, nids_ordered
def onTimeout(self): error = cgi.escape(self.pool) self.pool = "" self.mw.progress.clear() if "abortSchemaMod" in error: return if "Pyaudio not" in error: return showWarning(_("Please install PyAudio")) if "install mplayer" in error: return showWarning(_("Please install mplayer")) 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()) 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 = _( """\ An error occurred. It may have been caused by a harmless bug, <br> or your deck may have a problem. <p>To confirm it's not a problem with your deck, please run <b>Tools > Check Database</b>. <p>If that doesn't fix the problem, please copy the following<br> into a bug report:""" ) pluginText = _( """\ An error occurred in an add-on.<br> Please post on the add-on forum:<br>%s<br>""" ) pluginText %= "https://anki.tenderapp.com/discussions/add-ons" if "addon" in error: txt = pluginText else: txt = stdText # show dialog txt = txt + "<div style='white-space: pre-wrap'>" + error + "</div>" showText(txt, type="html")
def onRecSound(self): try: file = getAudio(self.widget) except Exception, e: showWarning(_( "Couldn't record audio. Have you installed lame and sox?") + "\n\n" + unicode(e)) return
def _checkForUnclosedWidgets(self): for w in self.app.topLevelWidgets(): if w.isVisible(): # windows with this property are safe to close immediately if getattr(w, "silentlyClose"): w.close() else: showWarning(f"Window should have been closed: {w}")
def onViewPage(self): addon = self.onlyOneSelected() if not addon: return if re.match(r"^\d+$", addon): openLink(aqt.appShared + "info/{}".format(addon)) else: showWarning(_("Add-on was not downloaded from AnkiWeb."))
def profileNameOk(self, str): from anki.utils import invalidFilename, invalidFilenameChars if invalidFilename(str): showWarning( _("A profile name cannot contain these characters: %s") % " ".join(invalidFilenameChars)) return return True
def onRecSound(self): try: file = getAudio(self.widget) except Exception as e: showWarning(_( "Couldn't record audio. Have you installed lame and sox?") + "\n\n" + repr(str(e))) return self.addMedia(file)
def onResetTimes(browser): # Make sure user selected something. if not browser.form.tableView.selectionModel().hasSelection(): showWarning("Please select at least one card to reset creation date.", parent=browser) return # Preprocess cards, collecting note IDs. (card_cnt, nids) = identifyNotes(browser.selectedCards()) # debug #showInfo(("Processed %s cards leading to %s notes") % (card_cnt, len(nids))) # Prompt for date. todaystr = time.strftime('%Y/%m/%d', time.localtime()) (s, ret) = getText("Enter a date as YYYY/MM/DD to set as the creation time, or 'today' for current date (%s):" % todaystr, parent=browser) if (not s) or (not ret): return # Generate a random MM:HH:SS. This will help prevent the same timestamp from # being used if this addon is executed multiple times with the same date. random_time = ("%s:%s:%s") % (random.randint(0, 23), random.randint(0, 59), random.randint(0, 59)) # Don't want random? Uncomment the following line and specify any time you # want in the format HH:MM:SS where HH is 00-24: #random_time = "15:01:01" if s == 'today': desttime = time.mktime(time.strptime(("%s %s") % (todaystr, random_time), '%Y/%m/%d %H:%M:%S')) else: try: desttime = time.mktime(time.strptime(("%s %s") % (s, random_time), '%Y/%m/%d %H:%M:%S')) except ValueError: showWarning("Sorry, I didn't understand that date. Please enter 'today' or a date in YYYY/MM/DD format", parent=browser) return # This mimics anki/utils.py timestampID function (which calls intTime for # seconds since epoch and multiplies those seconds by 1000). desttime = desttime * 1000 # debug # showInfo(("desttime %s") % desttime) # Force a full sync if collection isn't already marked for one. This is # apparently because we are changing the key column of the table. # (Per Damien on 2013/01/07: http://groups.google.com/group/anki-users/msg/3c8910e10f6fd0ac?hl=en ) mw.col.modSchema(check=True) # Do it. resetCreationTimes(nids, desttime) # Done. mw.reset() tooltip(ngettext("Creation time reset for %d note.", "Creation time reset for %d notes.", len(nids)) % len(nids))
def _uniqueName(self, prompt, ignoreOrd=None, old=""): txt = getOnlyText(prompt, default=old) if not txt: return for f in self.model['flds']: if ignoreOrd is not None and f['ord'] == ignoreOrd: continue if f['name'] == txt: showWarning(_("That field name is already used.")) return return txt
def _openBackup(self, path): try: # move the existing collection to the trash, as it may not open self.pm.trashCollection() except: showWarning( _( "Unable to move existing file to trash - please try restarting your computer." ) ) return self.pendingImport = path self.restoringBackup = True showInfo( _( """\ Automatic syncing and backups have been disabled while restoring. To enable them again, \ close the profile or restart Anki.""" ) ) self.onOpenProfile()
def onTimeout(self) -> None: error = html.escape(self.pool) self.pool = "" self.mw.progress.clear() if "abortSchemaMod" in error: return if "DeprecationWarning" in error: return if "10013" in error: showWarning(tr(TR.QT_MISC_YOUR_FIREWALL_OR_ANTIVIRUS_PROGRAM_IS)) return if "no default input" in error.lower(): showWarning(tr(TR.QT_MISC_PLEASE_CONNECT_A_MICROPHONE_AND_ENSURE)) return if "invalidTempFolder" in error: showWarning(self.tempFolderMsg()) return if "Beautiful Soup is not an HTTP client" in error: return if "database or disk is full" in error or "Errno 28" in error: showWarning(tr(TR.QT_MISC_YOUR_COMPUTERS_STORAGE_MAY_BE_FULL)) return if "disk I/O error" in error: showWarning(markdown(tr(TR.ERRORS_ACCESSING_DB))) return if self.mw.addonManager.dirty: txt = markdown(tr(TR.ERRORS_ADDONS_ACTIVE_POPUP)) error = f"{supportText() + self._addonText(error)}\n{error}" else: txt = markdown(tr(TR.ERRORS_STANDARD_POPUP)) error = f"{supportText()}\n{error}" # show dialog txt = f"{txt}<div style='white-space: pre-wrap'>{error}</div>" showText(txt, type="html", copyBtn=True)
def load_Group(self): config = mw.addonManager.getConfig(__name__) _translate = QtCore.QCoreApplication.translate url = 'https://ankileaderboard.pythonanywhere.com/groups/' try: Group_List = requests.get(url, timeout=20).json() except: Group_List = [] showWarning( "Timeout error [load_Group] - No internet connection, or server response took too long.", title="Leaderboard error") # item 0 is set by pyuic from the .ui file for i in range(1, len(Group_List) + 1): self.dialog.subject.addItem("") self.dialog.manageGroup.addItem("") index = 1 for i in Group_List: self.dialog.subject.setItemText(index, _translate("Dialog", i)) self.dialog.manageGroup.setItemText(index, _translate("Dialog", i)) index += 1 self.dialog.subject.setCurrentText(config["subject"])
def updateList(self, conf, key, w, minSize=1): items = str(w.text()).split(" ") ret = [] for i in items: if not i: continue #ADDON: Small potatoes if i.endswith('d'): i = int(float(i[:-1])) * 1440 try: i = float(i) assert i > 0 if i == int(i): i = int(i) ret.append(i) except: # invalid, don't update showWarning(_("Steps must be numbers.")) return if len(ret) < minSize: showWarning(_("At least one step is required.")) return conf[key] = ret
def updateList(self, conf: Any, key: str, w: QLineEdit, minSize: int = 1) -> None: items = str(w.text()).split(" ") ret = [] for item in items: if not item: continue try: i = float(item) assert i > 0 if i == int(i): i = int(i) ret.append(i) except: # invalid, don't update showWarning(_("Steps must be numbers.")) return if len(ret) < minSize: showWarning(_("At least one step is required.")) return conf[key] = ret
def updateList(self, conf: Any, key: str, w: QLineEdit, minSize: int = 1) -> None: items = str(w.text()).split(" ") ret = [] for item in items: if not item: continue try: i = float(item) assert i > 0 if i == int(i): i = int(i) ret.append(i) except: # invalid, don't update showWarning(tr(TR.SCHEDULING_STEPS_MUST_BE_NUMBERS)) return if len(ret) < minSize: showWarning(tr(TR.SCHEDULING_AT_LEAST_ONE_STEP_IS_REQUIRED)) return conf[key] = ret
def accept(self): file = getSaveFile(self, _("Export"), "export", self.exporter.key, self.exporter.ext) self.hide() if file: self.exporter.includeSched = (self.frm.includeSched.isChecked()) self.exporter.includeMedia = (self.frm.includeMedia.isChecked()) self.exporter.includeTags = (self.frm.includeTags.isChecked()) if not self.frm.deck.currentIndex(): self.exporter.did = None else: name = self.decks[self.frm.deck.currentIndex()] self.exporter.did = self.col.decks.id(name) self.mw.progress.start(immediate=True) try: f = open(file, "wb") f.close() except (OSError, IOError), e: showWarning(_("Couldn't save file: %s") % unicode(e)) else: os.unlink(file) self.exporter.exportInto(file) tooltip(_("%d exported.") % self.exporter.count) finally:
def addNote(self, note): note.model()['did'] = self.deckChooser.selectedId() ret = note.dupeOrEmpty() if ret == 1: showWarning(_("The first field is empty."), help="AddItems#AddError") return if '{{cloze:' in note.model()['tmpls'][0]['qfmt']: if not self.mw.col.models._availClozeOrds( note.model(), note.joinedFields(), False): if not askUser( _("You have a cloze deletion note type " "but have not made any cloze deletions. Proceed?")): return cards = self.mw.col.addNote(note) if not cards: showWarning(_("""\ The input you have provided would make an empty \ question on all cards."""), help="AddItems") return self.addHistory(note) self.mw.requireReset() return note
def onRemove(self): if len(self.model['tmpls']) < 2: return showInfo(_("At least one card type is required.")) idx = self.ord 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?") % dict(a=self.model['tmpls'][idx]['name'], b=cards)) if not askUser(msg): return if not self.mm.remTemplate(self.model, self.cards[idx].template()): return showWarning(_("""\ Removing this card type would cause one or more notes to be deleted. \ Please create a new card type first.""")) self.redraw()
def loadCollection(self): try: return self._loadCollection() except Exception as e: showWarning( _("""\ Anki was unable to open your collection file. If problems persist after \ restarting your computer, please use the Open Backup button in the profile \ manager. Debug info: """) + traceback.format_exc()) # clean up open collection if possible if self.col: try: self.col.close(save=False) except: pass self.col = None # return to profile manager self.hide() self.showProfileManager() return False
def onRemProfile(self): profs = self.pm.profiles() if len(profs) < 2: return showWarning(_("There must be at least one profile.")) # password correct? if not self.openProfile(): return # sure? if not askUser( _("""\ All cards, notes, and media for this profile will be deleted. \ Are you sure?""")): return self.pm.remove(self.pm.name) self.refreshProfilesList()
def clockIsOff(self, diff): diffText = ngettext("%s second", "%s seconds", diff) % diff warn = ( _( """\ In order to ensure your collection works correctly when moved between \ 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. 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()
def onDelete(self): if len(self.model["flds"]) < 2: return showWarning(tr(TR.FIELDS_NOTES_REQUIRE_AT_LEAST_ONE_FIELD)) count = self.mm.useCount(self.model) c = tr(TR.BROWSING_NOTE_COUNT, count=count) if not askUser(tr(TR.FIELDS_DELETE_FIELD_FROM, val=c)): return if not self.change_tracker.mark_schema(): return f = self.model["flds"][self.form.fieldList.currentRow()] self.mm.remove_field(self.model, f) gui_hooks.fields_did_delete_field(self, f) self.fillFields() self.form.fieldList.setCurrentRow(0)
def onRemProfile(self): profs = self.pm.profiles() if len(profs) < 2: return showWarning(_("There must be at least one profile.")) # sure? if not askUser( _("""\ All cards, notes, and media for this profile will be deleted. \ Are you sure?"""), msgfunc=QMessageBox.warning, defaultno=True, ): return self.pm.remove(self.pm.name) self.refreshProfilesList()
def remoteLangs(self, silent=False): self.mw.progress.start(immediate=True) try: langs = [] for entry in self._getRemoteDirectoryListing(): filename = entry["name"] info = self._parseFilename(filename) if info is None: continue filename, code, version = info verbose = self._langName(code) langs.append(Language(verbose, code, version, filename)) except IOError as e: if not silent: showWarning((_("Please check your internet connection.") + "\n\n" + str(e)), textFormat="plain") return finally: self.mw.progress.finish() mapped = OrderedDict((lang.code, lang) for lang in sorted(langs)) return mapped
def get_data(editor): "" "" # get word from current field word = stripHTMLMedia(editor.note.fields[editor.currentField]) clean_word = _normalize_word(word) audio_file = save_audio(clean_word) if (audio_file == None): showWarning( 'Vocabolaudio: no information found for the word: {}'.format(word)) return editor.addMedia(audio_file) for field in editor.note.keys(): if field == 'Audio' and audio_file != None: editor.note[field] = str('[sound:{}.{}]'.format(clean_word, 'mp3')) editor.note.flush() mw.reset()
def accept(self): "On close, create notes from the contents of the poem editor." title = self.form.titleBox.text().strip() if not title: showWarning("You must enter a title for this poem.") return if self.mw.col.findNotes(f'"note:{models.LpcgOne.name}" "Title:{title}"'): # pylint: disable=no-member showWarning("You already have a poem by that title in your " "database. Please check to see if you've already " "added it, or use a different name.") return if not self.form.textBox.toPlainText().strip(): showWarning("There's nothing to generate cards from! " "Please type a poem in the box, or use the " '"Open File" button to import a text file.') return tags = self.mw.col.tags.split(self.form.tagsBox.text()) text = cleanse_text(self.form.textBox.toPlainText().strip(), self.mw.addonManager.getConfig(__name__)) context_lines = self.form.contextLinesSpin.value() recite_lines = self.form.reciteLinesSpin.value() group_lines = self.form.groupLinesSpin.value() did = self.deckChooser.selectedId() try: notes_generated = add_notes(self.mw.col, Note, title, tags, text, did, context_lines, group_lines, recite_lines) except KeyError as e: showWarning( "The field {field} was not found on the {name} note type" " in your collection. If you don't have any LPCG notes" " yet, you can delete the note type in Tools -> Manage" " Note Types and restart Anki to fix this problem." " Otherwise, please add the field back to the note type. " .format(field=str(e), name=models.LpcgOne.name)) # pylint: disable=no-member return if notes_generated: super(LPCGDialog, self).accept() self.mw.reset() tooltip("%i notes added." % notes_generated)
def onTimeout(self) -> None: error = html.escape(self.pool) self.pool = "" self.mw.progress.clear() if "AbortSchemaModification" in error: return if "DeprecationWarning" in error: return if "10013" in error: showWarning(tr.qt_misc_your_firewall_or_antivirus_program_is()) return if "invalidTempFolder" in error: showWarning(self.tempFolderMsg()) return if "Beautiful Soup is not an HTTP client" in error: return if "database or disk is full" in error or "Errno 28" in error: showWarning(tr.qt_misc_your_computers_storage_may_be_full()) return if "disk I/O error" in error: showWarning(markdown(tr.errors_accessing_db())) return must_close = False if "PanicException" in error: must_close = True txt = markdown( "**A fatal error occurred, and Anki must close. Please report this message on the forums.**" ) error = f"{supportText() + self._addonText(error)}\n{error}" elif self.mw.addonManager.dirty: txt = markdown(tr.errors_addons_active_popup()) error = f"{supportText() + self._addonText(error)}\n{error}" else: txt = markdown(tr.errors_standard_popup()) error = f"{supportText()}\n{error}" # show dialog txt = f"{txt}<div style='white-space: pre-wrap'>{error}</div>" showText(txt, type="html", copyBtn=True) if must_close: sys.exit(1)
def onRemove(self): """ Remove the current template, except if it would leave a note without card. Ask user for confirmation""" # only difference: remove current index from newTemplatesData. if len(self.model['tmpls']) < 2: return showInfo(_("At least one card type is required.")) idx = self.ord 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?") % dict(a=self.model['tmpls'][idx]['name'], b=cards)) if not askUser(msg): return if not self.mm.remTemplate(self.model, self.cards[idx].template()): return showWarning(_("""\ Removing this card type would cause one or more notes to be deleted. \ Please create a new card type first.""")) del self.newTemplatesData[idx] # Only new line self.redraw()
def create_account(self): username = self.dialog.create_username.text() config = mw.addonManager.getConfig(__name__) url = 'https://ankileaderboard.pythonanywhere.com/allusers/' try: username_list = requests.get(url, timeout=20).json() except: showWarning( "Timeout error [create_account] - No internet connection, or server response took too long.", title="Leaderboard error") if username in username_list: tooltip("Username already taken") else: url = 'https://ankileaderboard.pythonanywhere.com/sync/' streak, cards, time, cards_past_30_days, retention, league_reviews, league_time, league_retention = Stats( self.season_start, self.season_end) config5 = config['subject'].replace(" ", "") config6 = config['country'].replace(" ", "") data = { 'Username': username, "Streak": streak, "Cards": cards, "Time": time, "Sync_Date": datetime.now(), "Month": cards_past_30_days, "Country": config["country"], "Retention": retention, "league_reviews": league_reviews, "league_time": league_time, "league_retention": league_retention, "Version": config["version"] } try: x = requests.post(url, data=data) if x.text == "Done!": tooltip("Successfully created account.") else: showWarning(str(x.text)) write_config("username", username) self.dialog.create_username.setText("") self.update_login_info(username) except: showWarning( "Timeout error [create_account] - No internet connection, or server response took too long.", title="Leaderboard error")
def manage_group(self): config = mw.addonManager.getConfig(__name__) group = self.dialog.manageGroup.currentText() oldPwd = self.dialog.oldPwd.text() newPwd = self.dialog.manage_newPwd.text() rPwd = self.dialog.manage_newRepeat.text() addAdmin = self.dialog.newAdmin.text() if newPwd != rPwd: showWarning("Passwords are not the same.") self.dialog.manage_newPwd.clear() self.dialog.manage_newRepeat.clear() return else: if oldPwd == "": oldPwd = None else: oldPwd = hashlib.sha1( oldPwd.encode('utf-8')).hexdigest().upper() if newPwd == "": newPwd = oldPwd else: newPwd = hashlib.sha1( newPwd.encode('utf-8')).hexdigest().upper() url = 'https://ankileaderboard.pythonanywhere.com/manageGroup/' data = { 'group': group, "user": config["username"], "token": config["token"], "oldPwd": oldPwd, "newPwd": newPwd, "addAdmin": addAdmin } try: x = requests.post(url, data=data, timeout=20) if x.text == "Done!": tooltip(f"{group} was updated successfully.") write_config("group_pwd", newPwd) if oldPwd != newPwd else write_config( "group_pwd", oldPwd) else: showWarning(str(x.text)) except: showWarning( "Timeout error [manage_group] - No internet connection, or server response took too long.", title="Leaderboard error")
def _delete(self, did): if str(did) == '1': return showWarning(_("The default deck can't be deleted.")) self.mw.checkpoint(_("Delete Deck")) deck = self.mw.col.decks.get(did) if not deck['dyn']: dids = [did] + [r[1] for r in self.mw.col.decks.children(did)] cnt = self.mw.col.db.scalar( "select count() from cards where did in {0} or " "odid in {0}".format(ids2str(dids))) if cnt: extra = _(" It has %d cards.") % cnt else: extra = None if deck['dyn'] or not extra or askUser( (_("Are you sure you wish to delete %s?") % deck['name']) + extra): self.mw.progress.start(immediate=True) self.mw.col.decks.rem(did, True) self.mw.progress.finish() self.show()
def queueExternal(path): global p, _player if mw.state == "review" and mw.reviewer.card != None and ( mw.reviewer.card.model()["name"] == "movies2anki (add-on)" or mw.reviewer.card.model()["name"].startswith( "movies2anki - subs2srs")): if mw.reviewer.state == "answer" and path.endswith(".mp4"): return try: clearExternalQueue() playVideoClip(path) except OSError: return showWarning( r"""<p>Please install <a href='https://mpv.io'>mpv</a>.</p> On Windows download mpv and either update PATH environment variable or put mpv.exe in Anki installation folder (C:\Program Files\Anki).""", parent=mw) else: _player(path)
def rename(self, name): oldName = self.name oldFolder = self.profileFolder() self.name = name newFolder = self.profileFolder(create=False) if os.path.exists(newFolder): if (oldFolder != newFolder) and (oldFolder.lower() == newFolder.lower()): # OS is telling us the folder exists because it does not take # case into account; use a temporary folder location midFolder = "".join([oldFolder, "-temp"]) if not os.path.exists(midFolder): os.rename(oldFolder, midFolder) oldFolder = midFolder else: showWarning( _("Please remove the folder %s and try again.") % midFolder ) self.name = oldName return else: showWarning(_("Folder already exists.")) self.name = oldName return # update name self.db.execute("update profiles set name = ? where name = ?", name, oldName) # rename folder try: os.rename(oldFolder, newFolder) except Exception as e: self.db.rollback() if "WinError 5" in str(e): showWarning( _( """\ Anki could not rename your profile because it could not rename the profile \ folder on disk. Please ensure you have permission to write to Documents/Anki \ and no other programs are accessing your profile folders, then try again.""" ) ) else: raise except: self.db.rollback() raise else: self.db.commit()
def _flatten(self, did): deck = self.mw.col.decks.get(did) if deck['dyn']: return showWarning(_("You can't flatten a dynamic deck")) self.mw.checkpoint(_("Flatten deck")) deck = self.mw.col.decks.get(did) dids = [r[1] for r in self.mw.col.decks.children(did)] cnt = self.mw.col.db.scalar( "select count() from cards where did in {0} or " "odid in {0}".format(ids2str(dids))) if cnt: extra = _(" Children decks have %d cards in total.") % cnt else: extra = None if not extra or askUser( (_("Are you sure you wish to flatten the deck %s?") % deck['name']) + extra): self.mw.progress.start(immediate=True) self.mw.col.decks.flatten(did) self.mw.progress.finish() self.show()
def _rename(self, did, newName=None): self.mw.checkpoint(_("Rename Deck")) deck = self.mw.col.decks.get(did) oldName = deck['name'] if newName is None: newName = getOnlyText(_("New deck name:"), default=oldName) newName = newName.replace('"', "") if not newName or newName == oldName: return try: if newName in self.mw.col.decks.allNames(): merge = askUser( _("The deck %s already exists. Do you want to merge %s in it ?" ) % (newName, oldName)) if merge: self.mw.col.decks.rename(deck, newName) else: self.mw.col.decks.rename(deck, newName) except DeckRenameError as e: return showWarning(e.description) self.show()
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 "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 or "Errno 28" 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: showWarning(markdown(tr(FString.ERRORS_ACCESSING_DB))) return if self.mw.addonManager.dirty: txt = markdown(tr(FString.ERRORS_ADDONS_ACTIVE_POPUP)) error = supportText() + self._addonText(error) + "\n" + error else: txt = markdown(tr(FString.ERRORS_STANDARD_POPUP)) error = supportText() + "\n" + error # show dialog txt = txt + "<div style='white-space: pre-wrap'>" + error + "</div>" showText(txt, type="html", copyBtn=True)
def _retrieveURL(self, url): "Download file into media folder and return local filename or None." # urllib doesn't understand percent-escaped utf8, but requires things like # '#' to be escaped. url = urllib.parse.unquote(url) if url.lower().startswith("file://"): url = url.replace("%", "%25") url = url.replace("#", "%23") local = True else: local = False # fetch it into a temporary folder self.mw.progress.start(immediate=not local, parent=self.parentWindow) ct = None try: if local: req = urllib.request.Request( url, None, {"User-Agent": "Mozilla/5.0 (compatible; Anki)"}) filecontents = urllib.request.urlopen(req).read() else: reqs = HttpClient() reqs.timeout = 30 r = reqs.get(url) if r.status_code != 200: showWarning( _("Unexpected response code: %s") % r.status_code) return filecontents = r.content ct = r.headers.get("content-type") except urllib.error.URLError as e: showWarning(_("An error occurred while opening %s") % e) return except requests.exceptions.RequestException as e: showWarning(_("An error occurred while opening %s") % e) return finally: self.mw.progress.finish() # strip off any query string url = re.sub(r"\?.*?$", "", url) fname = os.path.basename(urllib.parse.unquote(url)) if ct: fname = self.mw.col.media.add_extension_based_on_mime(fname, ct) return self.mw.col.media.write_data(fname, filecontents)
def _uniqueName(self, prompt, ignoreOrd=None, old=""): txt = getOnlyText(prompt, default=old).replace('"', "").strip() if not txt: return if txt[0] in "#^/": showWarning(tr(TR.FIELDS_NAME_FIRST_LETTER_NOT_VALID)) return for letter in """:{"}""": if letter in txt: showWarning(tr(TR.FIELDS_NAME_INVALID_LETTER)) return for f in self.model["flds"]: if ignoreOrd is not None and f["ord"] == ignoreOrd: continue if f["name"] == txt: showWarning(tr(TR.FIELDS_THAT_FIELD_NAME_IS_ALREADY_USED)) return return txt
def rename(self, name: str) -> None: oldName = self.name oldFolder = self.profileFolder() self.name = name newFolder = self.profileFolder(create=False) if os.path.exists(newFolder): if (oldFolder != newFolder) and (oldFolder.lower() == newFolder.lower()): # OS is telling us the folder exists because it does not take # case into account; use a temporary folder location midFolder = "".join([oldFolder, "-temp"]) if not os.path.exists(midFolder): os.rename(oldFolder, midFolder) oldFolder = midFolder else: showWarning( tr(TR.PROFILES_PLEASE_REMOVE_THE_FOLDER_AND, val=midFolder)) self.name = oldName return else: showWarning(tr(TR.PROFILES_FOLDER_ALREADY_EXISTS)) self.name = oldName return # update name self.db.execute("update profiles set name = ? where name = ?", name, oldName) # rename folder try: os.rename(oldFolder, newFolder) except Exception as e: self.db.rollback() if "WinError 5" in str(e): showWarning(tr(TR.PROFILES_ANKI_COULD_NOT_RENAME_YOUR_PROFILE)) else: raise except: self.db.rollback() raise else: self.db.commit()