Add streak display to overview window

This commit is contained in:
Al Ali 2025-06-16 18:22:18 +02:00
parent 8c19b1d9af
commit 64e2ff3cc5
5 changed files with 139 additions and 40 deletions

0
__init__.py Normal file
View file

0
anki_helpers/__init__.py Normal file
View file

29
anki_helpers/activity.py Normal file
View 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,
}

View file

@ -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 = {}

View file

@ -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()