#// 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 += ("%s | ") % (sidebar_font, "Date")
s += ("%s | " * 5) % ("Type", "Button", "Interval", "Ease", "Time")
cnt = 0
for (date, ease, ivl, factor, taken, type) in reversed(entries):
cnt += 1
s += "
---|
%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 += ("%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 += "
"
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()