#// auth_ Mohamad Janati #// Copyright (c) 2019-2021 Mohamad Janati (freaking stupid, right? :|) import os import io from anki.hooks import addHook from aqt.qt import * from aqt.webview import AnkiWebView import aqt.stats import time import datetime from anki.lang import _ from anki.utils import fmtTimeSpan from anki.stats import CardStats from aqt import * from anki.utils import htmlToTextLine from anki.collection import _Collection from aqt.reviewer import Reviewer #// sidebar functions class StatsSidebar(object): def __init__(self, mw): config = mw.addonManager.getConfig(__name__) sidebar_autoOpen = config['Card Info sidebar_ Auto Open'] self.mw = mw self.shown = False addHook("showQuestion", self._update) addHook("reviewCleanup", self._update) if sidebar_autoOpen: addHook("showQuestion", self.show) def _addDockable(self, title, w): class DockableWithClose(QDockWidget): closed = pyqtSignal() def closeEvent(self, evt): self.closed.emit() QDockWidget.closeEvent(self, evt) dock = DockableWithClose(title, mw) dock.setObjectName(title) dock.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea) dock.setFeatures(QDockWidget.AllDockWidgetFeatures) dock.setWidget(w) if mw.width() < 600: mw.resize(QSize(600, mw.height())) config = mw.addonManager.getConfig(__name__) sidebar_defaultPosition = config['Card Info sidebar_ Default Position'] if sidebar_defaultPosition == 1: mw.addDockWidget(Qt.LeftDockWidgetArea, dock) else: mw.addDockWidget(Qt.RightDockWidgetArea, dock) return dock def _remDockable(self, dock): mw.removeDockWidget(dock) def show(self): if not self.shown: class ThinAnkiWebView(AnkiWebView): def sizeHint(self): return QSize(200, 100) self.web = ThinAnkiWebView() self.shown = self._addDockable("Card Info", self.web) self.shown.closed.connect(self._onClosed) self._update() def hide(self): if self.shown: self._remDockable(self.shown) self.shown = None def toggle(self): if self.shown: self.hide() else: self.show() def _onClosed(self): # schedule removal for after evt has finished self.mw.progress.timer(100, self.hide, False) # modified _revlogData function def _revlogData_mod(self, card, cs): config = mw.addonManager.getConfig(__name__) sidebar_font = config['Card Info sidebar_ Font'] reviewsToShow = config['Card Info sidebar_ number of reviews to show for a card'] limited_review_warning_note = config['Card Info sidebar_ warning note'] custom_colors = config[' Review_ Custom Colors'] again_color = config['Color_ Again'] hard_color = config['Color_ Hard'] good_color = config['Color_ Good'] easy_color = config['Color_ Easy'] entries = self.mw.col.db.all("select id/1000.0, ease, ivl, factor, time/1000.0, type from revlog where cid = ?", card.id) if not entries: return "" s = "
Reviews
" s += ("") % (sidebar_font, "Date") s += ("" * 5) % ("Type", "Button", "Interval", "Ease", "Time") cnt = 0 for (date, ease, ivl, factor, taken, type) in reversed(entries): cnt += 1 s += "" % time.strftime("%y/%m/%d
%H:%M", time.localtime(date)) tstr = ["Learn", "Review", "Relearn", "Filtered", "Resched"][type] import anki.stats as st fmt = "%s" if type == 0: tstr = fmt % (st.colLearn, tstr) elif type == 1: tstr = fmt % (st.colMature, tstr) elif type == 2: tstr = fmt % (st.colRelearn, tstr) elif type == 3: tstr = fmt % (st.colCram, tstr) else: tstr = fmt % ("#000", tstr) if ease == 1: tstr = fmt % (st.colRelearn, tstr) #################### int_due = "%s" % time.strftime("%y/%m/%d", time.localtime(date)) if ivl > 0: int_due_date = time.localtime(date + (ivl * 24 * 60 * 60)) int_due = time.strftime("%y/%m/%d", int_due_date) #################### if ivl == 0: ivl = "0d" elif ivl > 0: ivl = fmtTimeSpan(ivl * 86400, short=True) else: ivl = cs.time(-ivl) if not custom_colors: again_color = "#FF1111" hard_color = "#FF9814" good_color = "#33FF2D" easy_color = "#21C0FF" if self.mw.col.sched_ver() == 1 and type == 3: if ease == 1: button = "
Again
".format(again_color) elif ease == 2: button = "
Good
".format(good_color) elif ease == 3: button = "
Good
".format(good_color) elif ease == 4: button = "
Easy
".format(easy_color) else: button = "ease: {}".format(ease) elif self.mw.col.sched_ver() == 1 and (type == 0 or type == 2): if ease == 1: button = "
Again
".format(again_color) elif ease == 2: button = "
Good
".format(good_color) elif ease == 3: button = "
Easy
".format(easy_color) elif ease == 4: button = "
Easy
".format(easy_color) else: button = "ease: {}".format(ease) else: if ease == 1: button = "
Again
".format(again_color) elif ease == 2: button = "
Hard
".format(hard_color) elif ease == 3: button = "
Good
".format(good_color) elif ease == 4: button = "
Easy
".format(easy_color) else: button = "ease: {}".format(ease) s += ("" * 5) % (tstr, button, "%s
(%s)" %(ivl, int_due), "%d%%" % (factor / 10) if factor else "", cs.time(taken)) + "" if reviewsToShow != 0: if cnt > int(reviewsToShow) - 1: break else: continue s += "
%s%s
%s%s
" warning = "" if limited_review_warning_note: if cnt < card.reps: try: a = int(reviewsToShow) warning = """

You have limited previous review information number to "{}" reviews.
""".format(reviewsToShow) except ValueError: warning = """

Some of the history is missing. For more information, please see the browser documentation.
""" return s + warning # adds the modified _revlogData function to Reviewer class in aqt.browser Reviewer._revlogData_mod = _revlogData_mod # modified report function def report_mod(self): from anki import version anki_version = int(version.replace('.', '')) if anki_version > 2119: from aqt.theme import theme_manager config = mw.addonManager.getConfig(__name__) infobar_created = config['Card Info sidebar_ Created'] infobar_edited = config['Card Info sidebar_ Edited'] infobar_firstReview = config['Card Info sidebar_ First Review'] infobar_latestReview = config['Card Info sidebar_ Latest Review'] infobar_due = config['Card Info sidebar_ Due'] infobar_interval = config['Card Info sidebar_ Interval'] infobar_ease = config['Card Info sidebar_ Ease'] infobar_reviews = config['Card Info sidebar_ Reviews'] infobar_lapses = config['Card Info sidebar_ Lapses'] infobar_correctPercent = config['Card Info Sidebar_ Correct Percent'] infobar_fastestReview = config['Card Info Sidebar_ Fastest Review'] infobar_slowestReview = config['Card Info Sidebar_ Slowest Review'] infobar_avgTime = config['Card Info sidebar_ Average Time'] infobar_totalTime = config['Card Info sidebar_ Total Time'] infobar_cardType = config['Card Info sidebar_ Card Type'] infobar_noteType = config['Card Info sidebar_ Note Type'] infobar_deck = config['Card Info sidebar_ Deck'] infobar_tags = config['Card Info sidebar_ Tags'] infobar_noteID = config['Card Info Sidebar_ Note ID'] infobar_cardID = config['Card Info Sidebar_ Card ID'] infobar_sortField = config['Card Info sidebar_ Sort Field'] c = self.card fmt = lambda x, **kwargs: fmtTimeSpan(x, short=True, **kwargs) self.txt = "" if infobar_created: self.addLine("Created", time.strftime("%Y-%m-%d | %H:%M", time.localtime(c.id/1000))) if infobar_edited: if c.note().mod != False and time.localtime(c.id/1000) != time.localtime(c.note().mod): self.addLine("Edited", time.strftime("%Y-%m-%d | %H:%M", time.localtime(c.note().mod))) first = self.col.db.scalar("select min(id) from revlog where cid = ?", c.id) last = self.col.db.scalar("select max(id) from revlog where cid = ?", c.id) if first: if infobar_firstReview: self.addLine("First Review", time.strftime("%Y-%m-%d | %H:%M", time.localtime(first/1000))) if infobar_latestReview: self.addLine("Latest Review", time.strftime("%Y-%m-%d | %H:%M", time.localtime(last/1000))) if c.type != 0: if c.odid or c.queue < 0: next = None else: if c.queue in (2,3): next = time.time()+((c.due - self.col.sched.today)*86400) else: next = c.due next = self.date(next) if next: if infobar_due: self.addLine("Due", next) if c.queue == 2: if infobar_interval: self.addLine("Interval", fmt(c.ivl * 86400)) if infobar_ease: self.addLine("Ease", "%d%%" % (c.factor/10.0)) if infobar_lapses: self.addLine("Lapses", "%d" % c.lapses) if self.col.sched_ver() == 1: pressed_again = mw.col.db.scalar("select sum(case when ease = 1 then 1 else 0 end) from revlog where cid = ?", c.id) pressed_good = mw.col.db.scalar("select sum(case when ease = 2 then 1 else 0 end) from revlog where cid = ?", c.id) pressed_easy = mw.col.db.scalar("select sum(case when ease = 3 then 1 else 0 end) from revlog where cid = ?", c.id) pressed_all = pressed_again + pressed_good + pressed_easy self.addLine("Again", "{} | {:.0f}%".format(str(pressed_again).rjust(4), float(pressed_again/pressed_all)*100)) self.addLine("Good", "{} | {:.0f}%".format(str(pressed_good).rjust(4), float(pressed_good/pressed_all)*100)) self.addLine("Easy", "{} | {:.0f}%".format(str(pressed_easy).rjust(4), float(pressed_easy/pressed_all)*100)) elif self.col.sched_ver() == 2: pressed_again = mw.col.db.scalar("select sum(case when ease = 1 then 1 else 0 end) from revlog where cid = ?", c.id) pressed_hard = mw.col.db.scalar("select sum(case when ease = 2 then 1 else 0 end) from revlog where cid = ?", c.id) pressed_good = mw.col.db.scalar("select sum(case when ease = 3 then 1 else 0 end) from revlog where cid = ?", c.id) pressed_easy = mw.col.db.scalar("select sum(case when ease = 4 then 1 else 0 end) from revlog where cid = ?", c.id) pressed_all = pressed_again + pressed_hard + pressed_good + pressed_easy if pressed_all < 1: pressed_all = 1 self.addLine("Again", "{} | {:.0f}%".format(str(pressed_again).rjust(4), float(pressed_again/pressed_all)*100)) self.addLine("Hard", "{} | {:.0f}%".format(str(pressed_hard).rjust(4), float(pressed_hard/pressed_all)*100)) self.addLine("Good", "{} | {:.0f}%".format(str(pressed_good).rjust(4), float(pressed_good/pressed_all)*100)) self.addLine("Easy", "{} | {:.0f}%".format(str(pressed_easy).rjust(4), float(pressed_easy/pressed_all)*100)) if infobar_reviews: self.addLine("Reviews", "%d" % c.reps) (cnt, total) = self.col.db.first("select count(), sum(time)/1000 from revlog where cid = ?", c.id) if infobar_correctPercent and c.reps > 0: self.addLine("Correct Percentage", "{:.0f}%".format(float((c.reps-c.lapses)/c.reps)*100)) if infobar_fastestReview: fastes_rev = mw.col.db.scalar("select time/1000.0 from revlog where cid = ? order by time asc limit 1", c.id) self.addLine("Fastest Review", self.time(fastes_rev)) if infobar_slowestReview: slowest_rev = mw.col.db.scalar("select time/1000.0 from revlog where cid = ? order by time desc limit 1", c.id) self.addLine("Slowest Review", self.time(slowest_rev)) if cnt: if infobar_avgTime: self.addLine("Average Time", self.time(total / float(cnt))) if infobar_totalTime: self.addLine("Total Time", self.time(total)) elif c.queue == 0: if infobar_due: self.addLine("Position", c.due) if infobar_cardType: self.addLine("Card Type", c.template()['name']) if infobar_noteType: self.addLine("Note Type", c.model()['name']) if infobar_noteID: self.addLine("Note ID", c.nid) if infobar_cardID: self.addLine("Card ID", c.id) if infobar_deck: self.addLine("Deck", self.col.decks.name(c.did)) if c.note().tags: if infobar_tags: self.addLine("Tags", " | ".join(c.note().tags)) f = c.note() sort_field = htmlToTextLine(f.fields[self.col.models.sortIdx(f.model())]) if infobar_sortField: if len(sort_field) > 40: self.addLine("Sort Field", "[{}
{}
{}...]".format(sort_field[:20], sort_field[20:41], sort_field[41:58])) else: self.addLine("Sort Field", htmlToTextLine(f.fields[self.col.models.sortIdx(f.model())])) self.txt += "
" return self.txt # adds the modified report functions to CardStats class in anki.stats CardStats.report_mod = report_mod # modified cardStats function def cardStats_mod(self, card): from anki.stats import CardStats return CardStats(self, card).report_mod() # adds a modified cardStats function to _Collection class in anki.collection _Collection.cardStats_mod = cardStats_mod # functions to get more previous cards to add them to sidebard def lastCard2(self): if self._answeredIds: if len(self._answeredIds) > 1: try: return self.mw.col.getCard(self._answeredIds[-2]) except TypeError: return def lastCard3(self): if self._answeredIds: if len(self._answeredIds) > 2: try: return self.mw.col.getCard(self._answeredIds[-3]) except TypeError: return def lastCard4(self): if self._answeredIds: if len(self._answeredIds) > 3: try: return self.mw.col.getCard(self._answeredIds[-4]) except TypeError: return # adds functions above to Reviewer class in aqt.reviewer Reviewer.lastCard2 = lastCard2 Reviewer.lastCard3 = lastCard3 Reviewer.lastCard4 = lastCard4 def _update(self): config = mw.addonManager.getConfig(__name__) infobar_currentReviewCount = config['Card Info sidebar_ Current Review Count'] try: sidebar_PreviousCards = int(config['Card Info sidebar_ Number of previous cards to show']) except ValueError: sidebar_PreviousCards = 2 if not self.shown: return txt = "" r = self.mw.reviewer d = self.mw.col cs = CardStats(d, r.card) current_card = r.card review_count = len(self.mw.reviewer._answeredIds) styles = """""" currentReviewCount = "
Current Card
Current Review Count: {}
".format(review_count) if current_card: txt += styles if infobar_currentReviewCount: txt += currentReviewCount else: txt += "
Current Card
" txt += d.cardStats_mod(current_card) txt += "

" txt += r._revlogData_mod(current_card, cs) card2 = r.lastCard() if card2 and sidebar_PreviousCards > 1: if sidebar_PreviousCards == 2: txt += "


Last Card
" else: txt += "
Card 2
" txt += d.cardStats_mod(card2) txt += "

" txt += r._revlogData_mod(card2, cs) if sidebar_PreviousCards < 3: if infobar_currentReviewCount: txt += currentReviewCount card3 = r.lastCard2() if card3 and sidebar_PreviousCards > 2: txt += "


Card 3
" txt += d.cardStats_mod(card3) txt += "

" txt += r._revlogData_mod(card3, cs) if sidebar_PreviousCards < 4: if infobar_currentReviewCount: txt += currentReviewCount card4 = r.lastCard3() if card4 and sidebar_PreviousCards > 3: txt += "


Card 4
" txt += d.cardStats_mod(card4) txt += "

" txt += r._revlogData_mod(card4, cs) if infobar_currentReviewCount: txt += currentReviewCount if not txt: styles = """""" txt = styles card2 = r.lastCard() if card2 and sidebar_PreviousCards > 1: txt += "

Last Card
" txt += d.cardStats_mod(card2) txt += "

" txt += r._revlogData_mod(card2, cs) if sidebar_PreviousCards < 3: if infobar_currentReviewCount: txt += currentReviewCount card3 = r.lastCard2() if card3 and sidebar_PreviousCards > 2: txt += "


Card 2
" txt += d.cardStats_mod(card3) txt += "

" txt += r._revlogData_mod(card3, cs) if sidebar_PreviousCards < 4: if infobar_currentReviewCount: txt += currentReviewCount card4 = r.lastCard3() if card4 and sidebar_PreviousCards > 3: txt += "


Card 3
" txt += d.cardStats_mod(card4) txt += "

" txt += r._revlogData_mod(card4, cs) if infobar_currentReviewCount: txt += currentReviewCount style = self._style() self.web.setHtml("""

%s
"""% (style, txt)) def _style(self): from anki import version anki_version = int(version.replace('.', '')) if anki_version > 2119: from aqt.theme import theme_manager config = mw.addonManager.getConfig(__name__) sidebar_theme = config['Card Info sidebar_ theme'] sidebar_font = config['Card Info sidebar_ Font'] from . import styles dark_styles = styles.dark light_styles = styles.light if anki_version > 2119: if sidebar_theme == 2: mystyle = dark_styles elif sidebar_theme == 1: mystyle = light_styles else: if theme_manager.night_mode: mystyle = dark_styles else: mystyle = light_styles else: if sidebar_theme == 2: mystyle = dark_styles else: mystyle = light_styles from anki import version if version.startswith("2.0."): return "" return mystyle + "td { font-size: 75%; font-family:" + "{}".format(sidebar_font) + ";}" _cs = StatsSidebar(mw) def cardStats(on): _cs.toggle()