mirror of
https://github.com/ankitects/anki.git
synced 2026-01-13 14:03:55 -05:00
Add streak display to overview window
This commit is contained in:
parent
8c19b1d9af
commit
64e2ff3cc5
5 changed files with 139 additions and 40 deletions
0
__init__.py
Normal file
0
__init__.py
Normal file
0
anki_helpers/__init__.py
Normal file
0
anki_helpers/__init__.py
Normal file
29
anki_helpers/activity.py
Normal file
29
anki_helpers/activity.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
from datetime import datetime, timedelta
|
||||
def analyze_activity(col) -> dict:
|
||||
"""Analysiert die Aktivität der letzten 30 Tage anhand des Revlogs."""
|
||||
cutoff = (col.sched.day_cutoff - 86400 * 30) * 1000
|
||||
|
||||
# Hol alle Review-Einträge der letzten 30 Tage
|
||||
rows = col.db.all("""
|
||||
SELECT id FROM revlog
|
||||
WHERE id > ? AND type IN (0, 1, 2, 3, 4)
|
||||
""", cutoff)
|
||||
|
||||
# Extrahiere das Datum aus den IDs (ms seit Unix-Zeit)
|
||||
dates = [datetime.fromtimestamp(row[0] / 1000).date() for row in rows]
|
||||
|
||||
# Zähle, an wie vielen Tagen es Aktivität gab
|
||||
day_counts = {}
|
||||
for date in dates:
|
||||
day_counts[date] = day_counts.get(date, 0) + 1
|
||||
|
||||
active_days = len(day_counts)
|
||||
total_reviews = sum(day_counts.values())
|
||||
average = total_reviews / 30
|
||||
low_days = sum(1 for v in day_counts.values() if v < 20)
|
||||
|
||||
return {
|
||||
"active_days": active_days,
|
||||
"average_per_day": round(average, 1),
|
||||
"low_days": low_days,
|
||||
}
|
||||
|
|
@ -5,6 +5,17 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys, os
|
||||
|
||||
# Dynamisch den Pfad zu 'anki_helpers' ergänzen
|
||||
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
|
||||
anki_helpers_path = os.path.join(project_root, "anki")
|
||||
sys.path.insert(0, anki_helpers_path)
|
||||
|
||||
from anki_helpers.activity import analyze_activity
|
||||
|
||||
|
||||
|
||||
import json
|
||||
import random
|
||||
import time
|
||||
|
|
@ -140,7 +151,11 @@ body { direction: ltr !important; }
|
|||
######################################################################
|
||||
|
||||
def todayStats(self) -> str:
|
||||
|
||||
print("todayStats wurde aufgerufen")
|
||||
b = self._title("Today")
|
||||
|
||||
|
||||
# studied today
|
||||
lim = self._revlogLimit()
|
||||
if lim:
|
||||
|
|
@ -156,7 +171,7 @@ sum(case when type = {REVLOG_CRAM} then 1 else 0 end) /* filter */
|
|||
from revlog where type != {REVLOG_RESCHED} and id > ? """
|
||||
+ lim,
|
||||
(self.col.sched.day_cutoff - 86400) * 1000,
|
||||
)
|
||||
)
|
||||
cards = cards or 0
|
||||
thetime = thetime or 0
|
||||
failed = failed or 0
|
||||
|
|
@ -191,7 +206,7 @@ from revlog where type != {REVLOG_RESCHED} and id > ? """
|
|||
where lastIvl >= 21 and id > ?"""
|
||||
+ lim,
|
||||
(self.col.sched.day_cutoff - 86400) * 1000,
|
||||
)
|
||||
)
|
||||
b += "<br>"
|
||||
if mcnt:
|
||||
b += "Correct answers on mature cards: %(a)d/%(b)d (%(c).1f%%)" % dict(
|
||||
|
|
@ -201,8 +216,23 @@ from revlog where type != {REVLOG_RESCHED} and id > ? """
|
|||
b += "No mature cards were studied today."
|
||||
else:
|
||||
b += "No cards have been studied today."
|
||||
|
||||
activity = analyze_activity(self.col)
|
||||
b += "<hr><b>Activity (last 30 days):</b><br>"
|
||||
b += f"Active days: {activity['active_days']}<br>"
|
||||
b += f"Average/day: {activity['average_per_day']}<br>"
|
||||
b += f"Low activity days: {activity['low_days']}<br>"
|
||||
b += "<br><b>TEST123 - wird dieser Text angezeigt?</b><br>"
|
||||
b += "<hr><h3 style='color:red'>TEST123 sichtbar?</h3><br>"
|
||||
b += "<hr><b>Activity (last 30 days):</b><br>"
|
||||
b += "Active days: 10<br>"
|
||||
b += "Average/day: 5.5<br>"
|
||||
b += "Low activity days: 3<br>"
|
||||
|
||||
|
||||
return b
|
||||
|
||||
|
||||
# Due and cumulative due
|
||||
######################################################################
|
||||
|
||||
|
|
@ -281,13 +311,13 @@ select count() from cards where did in %s and queue in ({QUEUE_TYPE_REV},{QUEUE_
|
|||
and due = ?"""
|
||||
% self._limit(),
|
||||
self.col.sched.today + 1,
|
||||
)
|
||||
)
|
||||
tomorrow = "%d cards" % tomorrow
|
||||
self._line(i, "Due tomorrow", tomorrow)
|
||||
return self._lineTbl(i)
|
||||
|
||||
def _due(
|
||||
self, start: int | None = None, end: int | None = None, chunk: int = 1
|
||||
self, start: int | None = None, end: int | None = None, chunk: int = 1
|
||||
) -> Any:
|
||||
lim = ""
|
||||
if start is not None:
|
||||
|
|
@ -306,7 +336,7 @@ group by day order by day"""
|
|||
% (self._limit(), lim),
|
||||
self.col.sched.today,
|
||||
chunk,
|
||||
)
|
||||
)
|
||||
|
||||
# Added, reps and time spent
|
||||
######################################################################
|
||||
|
|
@ -406,13 +436,13 @@ group by day order by day"""
|
|||
return self._section(txt1) + self._section(txt2)
|
||||
|
||||
def _ansInfo(
|
||||
self,
|
||||
totd: list[tuple[int, float]],
|
||||
studied: int,
|
||||
first: int,
|
||||
unit: str,
|
||||
convHours: bool = False,
|
||||
total: int | None = None,
|
||||
self,
|
||||
totd: list[tuple[int, float]],
|
||||
studied: int,
|
||||
first: int,
|
||||
unit: str,
|
||||
convHours: bool = False,
|
||||
total: int | None = None,
|
||||
) -> tuple[str, int]:
|
||||
assert totd
|
||||
tot = totd[-1][1]
|
||||
|
|
@ -427,7 +457,7 @@ group by day order by day"""
|
|||
"<b>%(pct)d%%</b> (%(x)s of %(y)s)"
|
||||
% dict(x=studied, y=period, pct=studied / float(period) * 100),
|
||||
bold=False,
|
||||
)
|
||||
)
|
||||
if convHours:
|
||||
tunit = "hours"
|
||||
else:
|
||||
|
|
@ -454,9 +484,9 @@ group by day order by day"""
|
|||
return self._lineTbl(i), int(tot)
|
||||
|
||||
def _splitRepData(
|
||||
self,
|
||||
data: list[tuple[Any, ...]],
|
||||
spec: Sequence[tuple[int, str, str]],
|
||||
self,
|
||||
data: list[tuple[Any, ...]],
|
||||
spec: Sequence[tuple[int, str, str]],
|
||||
) -> tuple[list[dict[str, Any]], list[tuple[Any, Any]]]:
|
||||
sep: dict[int, Any] = {}
|
||||
totcnt = {}
|
||||
|
|
@ -519,7 +549,7 @@ group by day order by day"""
|
|||
% lim,
|
||||
self.col.sched.day_cutoff,
|
||||
chunk,
|
||||
)
|
||||
)
|
||||
|
||||
def _done(self, num: int | None = 7, chunk: int = 1) -> Any:
|
||||
lims = []
|
||||
|
|
@ -563,7 +593,7 @@ group by day order by day"""
|
|||
tf,
|
||||
tf,
|
||||
tf,
|
||||
)
|
||||
)
|
||||
|
||||
def _daysStudied(self) -> Any:
|
||||
lims = []
|
||||
|
|
@ -587,7 +617,7 @@ from revlog %s
|
|||
group by day order by day)"""
|
||||
% lim,
|
||||
self.col.sched.day_cutoff,
|
||||
)
|
||||
)
|
||||
assert ret
|
||||
return ret
|
||||
|
||||
|
|
@ -647,7 +677,7 @@ group by grp
|
|||
order by grp"""
|
||||
% (self._limit(), lim),
|
||||
chunk,
|
||||
)
|
||||
)
|
||||
]
|
||||
return (
|
||||
data
|
||||
|
|
@ -729,11 +759,11 @@ select count(), avg(ivl), max(ivl) from cards where did in %s and queue = {QUEUE
|
|||
% dict(pct=pct, good=good, tot=tot)
|
||||
)
|
||||
return (
|
||||
"""
|
||||
<center><table width=%dpx><tr><td width=50></td><td align=center>"""
|
||||
% self.width
|
||||
+ "</td><td align=center>".join(i)
|
||||
+ "</td></tr></table></center>"
|
||||
"""
|
||||
<center><table width=%dpx><tr><td width=50></td><td align=center>"""
|
||||
% self.width
|
||||
+ "</td><td align=center>".join(i)
|
||||
+ "</td></tr></table></center>"
|
||||
)
|
||||
|
||||
def _eases(self) -> Any:
|
||||
|
|
@ -852,7 +882,7 @@ from revlog where type in ({REVLOG_LRN},{REVLOG_REV},{REVLOG_RELRN}) %s
|
|||
group by hour having count() > 30 order by hour"""
|
||||
% lim,
|
||||
self.col.sched.day_cutoff - (rolloverHour * 3600),
|
||||
)
|
||||
)
|
||||
|
||||
# Cards
|
||||
######################################################################
|
||||
|
|
@ -862,12 +892,12 @@ group by hour having count() > 30 order by hour"""
|
|||
div = self._cards()
|
||||
d = []
|
||||
for c, (t, col) in enumerate(
|
||||
(
|
||||
("Mature", colMature),
|
||||
("Young+Learn", colYoung),
|
||||
("Unseen", colUnseen),
|
||||
("Suspended+Buried", colSusp),
|
||||
)
|
||||
(
|
||||
("Mature", colMature),
|
||||
("Young+Learn", colYoung),
|
||||
("Unseen", colUnseen),
|
||||
("Suspended+Buried", colSusp),
|
||||
)
|
||||
):
|
||||
d.append(dict(data=div[c], label=f"{t}: {div[c]}", color=col))
|
||||
# text data
|
||||
|
|
@ -957,14 +987,14 @@ from cards where did in %s"""
|
|||
######################################################################
|
||||
|
||||
def _graph(
|
||||
self,
|
||||
id: str,
|
||||
data: Any,
|
||||
conf: Any | None = None,
|
||||
type: str = "bars",
|
||||
xunit: int = 1,
|
||||
ylabel: str = "Cards",
|
||||
ylabel2: str = "",
|
||||
self,
|
||||
id: str,
|
||||
data: Any,
|
||||
conf: Any | None = None,
|
||||
type: str = "bars",
|
||||
xunit: int = 1,
|
||||
ylabel: str = "Cards",
|
||||
ylabel2: str = "",
|
||||
) -> str:
|
||||
if conf is None:
|
||||
conf = {}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ from aqt.operations.scheduling import (
|
|||
from aqt.sound import av_player
|
||||
from aqt.toolbar import BottomBar
|
||||
from aqt.utils import askUserDialog, openLink, shortcut, tooltip, tr
|
||||
from aqt.qt import QLabel,QHBoxLayout, QWidget
|
||||
|
||||
|
||||
class OverviewBottomBar:
|
||||
|
|
@ -56,6 +57,30 @@ class Overview:
|
|||
self.bottom = BottomBar(mw, mw.bottomWeb)
|
||||
self._refresh_needed = False
|
||||
|
||||
# 🔥 Streak-Label erstellen
|
||||
self.streak_label = QLabel()
|
||||
self.streak_label.setText("") # wird später gesetzt
|
||||
self.streak_label.setStyleSheet("""
|
||||
font-size: 16px;
|
||||
padding: 10px;
|
||||
color: "b";
|
||||
font-weight: bold;
|
||||
qproperty-alignment: AlignCenter;
|
||||
""")
|
||||
|
||||
# 📐 Horizontales Layout zur Zentrierung
|
||||
streak_layout = QHBoxLayout()
|
||||
streak_layout.addStretch()
|
||||
streak_layout.addWidget(self.streak_label)
|
||||
streak_layout.addStretch()
|
||||
|
||||
# 🧱 Wrapper-Widget mit Layout
|
||||
streak_widget = QWidget()
|
||||
streak_widget.setLayout(streak_layout)
|
||||
|
||||
# ⬆️ In das Hauptlayout einfügen – ganz oben
|
||||
self.web.layout().insertWidget(0, streak_widget)
|
||||
|
||||
def show(self) -> None:
|
||||
av_player.stop_and_clear_queue()
|
||||
self.web.set_bridge_command(self._linkHandler, self)
|
||||
|
|
@ -68,12 +93,27 @@ class Overview:
|
|||
self._renderPage()
|
||||
self._renderBottom()
|
||||
self.mw.web.setFocus()
|
||||
streak_days = self.mw.col.db.scalar(
|
||||
"""
|
||||
SELECT COUNT(*) FROM (
|
||||
SELECT id FROM revlog
|
||||
WHERE id > strftime('%s', 'now', '-30 days')*1000
|
||||
GROUP BY strftime('%Y-%m-%d', id/1000, 'unixepoch')
|
||||
)
|
||||
"""
|
||||
)
|
||||
self.streak_label.setText(f" Streak: {streak_days} Tage")
|
||||
tooltip = f" Streak: {streak_days} Tage"
|
||||
self.streak_label.setText(f" Streak: {streak_days} Tage")
|
||||
|
||||
gui_hooks.overview_did_refresh(self)
|
||||
|
||||
QueryOp(
|
||||
parent=self.mw, op=lambda col: col.sched.counts(), success=success
|
||||
).run_in_background()
|
||||
|
||||
|
||||
|
||||
def refresh_if_needed(self) -> None:
|
||||
if self._refresh_needed:
|
||||
self.refresh()
|
||||
|
|
|
|||
Loading…
Reference in a new issue