1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
|
#// 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 = "<div style='text-align: center; font-family: arial; font-weight: bold;'> Reviews </div>"
s += ("<style>th {font-family: %s; font-size: 13px;}</style><table width=100%% ><tr><th align=left>%s</th>") % (sidebar_font, "Date")
s += ("<th align=center >%s</th>" * 5) % ("Type", "Button", "Interval", "Ease", "Time")
cnt = 0
for (date, ease, ivl, factor, taken, type) in reversed(entries):
cnt += 1
s += "<tr><td>%s</td>" % time.strftime("<b>%y/%m/%d</b><br>%H:%M", time.localtime(date))
tstr = ["Learn", "Review", "Relearn", "Filtered", "Resched"][type]
import anki.stats as st
fmt = "<span style='color:%s'>%s</span>"
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 = "<div style='color: {};'>Again</div>".format(again_color)
elif ease == 2:
button = "<div style='color: {};'>Good</div>".format(good_color)
elif ease == 3:
button = "<div style='color: {};'>Good</div>".format(good_color)
elif ease == 4:
button = "<div style='color: {};'>Easy</div>".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 = "<div style='color: {};'>Again</div>".format(again_color)
elif ease == 2:
button = "<div style='color: {};'>Good</div>".format(good_color)
elif ease == 3:
button = "<div style='color: {};'>Easy</div>".format(easy_color)
elif ease == 4:
button = "<div style='color: {};'>Easy</div>".format(easy_color)
else:
button = "ease: {}".format(ease)
else:
if ease == 1:
button = "<div style='color: {};'>Again</div>".format(again_color)
elif ease == 2:
button = "<div style='color: {};'>Hard</div>".format(hard_color)
elif ease == 3:
button = "<div style='color: {};'>Good</div>".format(good_color)
elif ease == 4:
button = "<div style='color: {};'>Easy</div>".format(easy_color)
else:
button = "ease: {}".format(ease)
s += ("<td align=center>%s</td>" * 5) % (tstr, button, "%s<br>(%s)" %(ivl, int_due), "%d%%" % (factor / 10) if factor else "", cs.time(taken)) + "</tr>"
if reviewsToShow != 0:
if cnt > int(reviewsToShow) - 1:
break
else:
continue
s += "</table>"
warning = ""
if limited_review_warning_note:
if cnt < card.reps:
try:
a = int(reviewsToShow)
warning = """<div style="font-family: consolas; font-size: 12px;"><hr> You have limited previous review information number to "{}" reviews.</div>""".format(reviewsToShow)
except ValueError:
warning = """<div style="font-family: consolas; font-size: 12px;"><hr>Some of the history is missing. For more information, please see the browser documentation.</div>"""
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 = "<table width=100%>"
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", "[{}<br>{}<br>{}...]".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 += "</table>"
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 = """<style>
.title {
font-family: arial;
padding-bottom: 15px;
font-weight: bold;
}</style>"""
currentReviewCount = "<div class='title'>Current Card</div><div style='font-family: courier; font-size: 10px;'>Current Review Count: {}</div>".format(review_count)
if current_card:
txt += styles
if infobar_currentReviewCount:
txt += currentReviewCount
else:
txt += "<div class='title'>Current Card</div>"
txt += d.cardStats_mod(current_card)
txt += "<p>"
txt += r._revlogData_mod(current_card, cs)
card2 = r.lastCard()
if card2 and sidebar_PreviousCards > 1:
if sidebar_PreviousCards == 2:
txt += "<hr><div class='title'>Last Card</div>"
else:
txt += "<hr><div class='title'>Card 2</div>"
txt += d.cardStats_mod(card2)
txt += "<p>"
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 += "<hr><div class='title''>Card 3</div>"
txt += d.cardStats_mod(card3)
txt += "<p>"
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 += "<hr><div class='title''>Card 4</div>"
txt += d.cardStats_mod(card4)
txt += "<p>"
txt += r._revlogData_mod(card4, cs)
if infobar_currentReviewCount:
txt += currentReviewCount
if not txt:
styles = """<style>
.title {
font-family: arial;
padding-bottom: 15px;
font-weight: bold;
}</style>"""
txt = styles
card2 = r.lastCard()
if card2 and sidebar_PreviousCards > 1:
txt += "<div class='title'>Last Card</div>"
txt += d.cardStats_mod(card2)
txt += "<p>"
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 += "<hr><div class='title''>Card 2</div>"
txt += d.cardStats_mod(card3)
txt += "<p>"
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 += "<hr><div class='title''>Card 3</div>"
txt += d.cardStats_mod(card4)
txt += "<p>"
txt += r._revlogData_mod(card4, cs)
if infobar_currentReviewCount:
txt += currentReviewCount
style = self._style()
self.web.setHtml("""
<html>
<head>
<style>%s</style>
</head>
<body>
<center>%s</center>
</body>
</html>
"""% (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()
|