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 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 json
import random import random
import time import time
@ -140,7 +151,11 @@ body { direction: ltr !important; }
###################################################################### ######################################################################
def todayStats(self) -> str: def todayStats(self) -> str:
print("todayStats wurde aufgerufen")
b = self._title("Today") b = self._title("Today")
# studied today # studied today
lim = self._revlogLimit() lim = self._revlogLimit()
if lim: 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 > ? """ from revlog where type != {REVLOG_RESCHED} and id > ? """
+ lim, + lim,
(self.col.sched.day_cutoff - 86400) * 1000, (self.col.sched.day_cutoff - 86400) * 1000,
) )
cards = cards or 0 cards = cards or 0
thetime = thetime or 0 thetime = thetime or 0
failed = failed or 0 failed = failed or 0
@ -191,7 +206,7 @@ from revlog where type != {REVLOG_RESCHED} and id > ? """
where lastIvl >= 21 and id > ?""" where lastIvl >= 21 and id > ?"""
+ lim, + lim,
(self.col.sched.day_cutoff - 86400) * 1000, (self.col.sched.day_cutoff - 86400) * 1000,
) )
b += "<br>" b += "<br>"
if mcnt: if mcnt:
b += "Correct answers on mature cards: %(a)d/%(b)d (%(c).1f%%)" % dict( 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." b += "No mature cards were studied today."
else: else:
b += "No cards have been studied today." 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 return b
# Due and cumulative due # 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 = ?""" and due = ?"""
% self._limit(), % self._limit(),
self.col.sched.today + 1, self.col.sched.today + 1,
) )
tomorrow = "%d cards" % tomorrow tomorrow = "%d cards" % tomorrow
self._line(i, "Due tomorrow", tomorrow) self._line(i, "Due tomorrow", tomorrow)
return self._lineTbl(i) return self._lineTbl(i)
def _due( 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: ) -> Any:
lim = "" lim = ""
if start is not None: if start is not None:
@ -306,7 +336,7 @@ group by day order by day"""
% (self._limit(), lim), % (self._limit(), lim),
self.col.sched.today, self.col.sched.today,
chunk, chunk,
) )
# Added, reps and time spent # Added, reps and time spent
###################################################################### ######################################################################
@ -406,13 +436,13 @@ group by day order by day"""
return self._section(txt1) + self._section(txt2) return self._section(txt1) + self._section(txt2)
def _ansInfo( def _ansInfo(
self, self,
totd: list[tuple[int, float]], totd: list[tuple[int, float]],
studied: int, studied: int,
first: int, first: int,
unit: str, unit: str,
convHours: bool = False, convHours: bool = False,
total: int | None = None, total: int | None = None,
) -> tuple[str, int]: ) -> tuple[str, int]:
assert totd assert totd
tot = totd[-1][1] tot = totd[-1][1]
@ -427,7 +457,7 @@ group by day order by day"""
"<b>%(pct)d%%</b> (%(x)s of %(y)s)" "<b>%(pct)d%%</b> (%(x)s of %(y)s)"
% dict(x=studied, y=period, pct=studied / float(period) * 100), % dict(x=studied, y=period, pct=studied / float(period) * 100),
bold=False, bold=False,
) )
if convHours: if convHours:
tunit = "hours" tunit = "hours"
else: else:
@ -454,9 +484,9 @@ group by day order by day"""
return self._lineTbl(i), int(tot) return self._lineTbl(i), int(tot)
def _splitRepData( def _splitRepData(
self, self,
data: list[tuple[Any, ...]], data: list[tuple[Any, ...]],
spec: Sequence[tuple[int, str, str]], spec: Sequence[tuple[int, str, str]],
) -> tuple[list[dict[str, Any]], list[tuple[Any, Any]]]: ) -> tuple[list[dict[str, Any]], list[tuple[Any, Any]]]:
sep: dict[int, Any] = {} sep: dict[int, Any] = {}
totcnt = {} totcnt = {}
@ -519,7 +549,7 @@ group by day order by day"""
% lim, % lim,
self.col.sched.day_cutoff, self.col.sched.day_cutoff,
chunk, chunk,
) )
def _done(self, num: int | None = 7, chunk: int = 1) -> Any: def _done(self, num: int | None = 7, chunk: int = 1) -> Any:
lims = [] lims = []
@ -563,7 +593,7 @@ group by day order by day"""
tf, tf,
tf, tf,
tf, tf,
) )
def _daysStudied(self) -> Any: def _daysStudied(self) -> Any:
lims = [] lims = []
@ -587,7 +617,7 @@ from revlog %s
group by day order by day)""" group by day order by day)"""
% lim, % lim,
self.col.sched.day_cutoff, self.col.sched.day_cutoff,
) )
assert ret assert ret
return ret return ret
@ -647,7 +677,7 @@ group by grp
order by grp""" order by grp"""
% (self._limit(), lim), % (self._limit(), lim),
chunk, chunk,
) )
] ]
return ( return (
data 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) % dict(pct=pct, good=good, tot=tot)
) )
return ( return (
""" """
<center><table width=%dpx><tr><td width=50></td><td align=center>""" <center><table width=%dpx><tr><td width=50></td><td align=center>"""
% self.width % self.width
+ "</td><td align=center>".join(i) + "</td><td align=center>".join(i)
+ "</td></tr></table></center>" + "</td></tr></table></center>"
) )
def _eases(self) -> Any: 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""" group by hour having count() > 30 order by hour"""
% lim, % lim,
self.col.sched.day_cutoff - (rolloverHour * 3600), self.col.sched.day_cutoff - (rolloverHour * 3600),
) )
# Cards # Cards
###################################################################### ######################################################################
@ -862,12 +892,12 @@ group by hour having count() > 30 order by hour"""
div = self._cards() div = self._cards()
d = [] d = []
for c, (t, col) in enumerate( for c, (t, col) in enumerate(
( (
("Mature", colMature), ("Mature", colMature),
("Young+Learn", colYoung), ("Young+Learn", colYoung),
("Unseen", colUnseen), ("Unseen", colUnseen),
("Suspended+Buried", colSusp), ("Suspended+Buried", colSusp),
) )
): ):
d.append(dict(data=div[c], label=f"{t}: {div[c]}", color=col)) d.append(dict(data=div[c], label=f"{t}: {div[c]}", color=col))
# text data # text data
@ -957,14 +987,14 @@ from cards where did in %s"""
###################################################################### ######################################################################
def _graph( def _graph(
self, self,
id: str, id: str,
data: Any, data: Any,
conf: Any | None = None, conf: Any | None = None,
type: str = "bars", type: str = "bars",
xunit: int = 1, xunit: int = 1,
ylabel: str = "Cards", ylabel: str = "Cards",
ylabel2: str = "", ylabel2: str = "",
) -> str: ) -> str:
if conf is None: if conf is None:
conf = {} conf = {}

View file

@ -22,6 +22,7 @@ from aqt.operations.scheduling import (
from aqt.sound import av_player from aqt.sound import av_player
from aqt.toolbar import BottomBar from aqt.toolbar import BottomBar
from aqt.utils import askUserDialog, openLink, shortcut, tooltip, tr from aqt.utils import askUserDialog, openLink, shortcut, tooltip, tr
from aqt.qt import QLabel,QHBoxLayout, QWidget
class OverviewBottomBar: class OverviewBottomBar:
@ -56,6 +57,30 @@ class Overview:
self.bottom = BottomBar(mw, mw.bottomWeb) self.bottom = BottomBar(mw, mw.bottomWeb)
self._refresh_needed = False 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: def show(self) -> None:
av_player.stop_and_clear_queue() av_player.stop_and_clear_queue()
self.web.set_bridge_command(self._linkHandler, self) self.web.set_bridge_command(self._linkHandler, self)
@ -68,12 +93,27 @@ class Overview:
self._renderPage() self._renderPage()
self._renderBottom() self._renderBottom()
self.mw.web.setFocus() 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) gui_hooks.overview_did_refresh(self)
QueryOp( QueryOp(
parent=self.mw, op=lambda col: col.sched.counts(), success=success parent=self.mw, op=lambda col: col.sched.counts(), success=success
).run_in_background() ).run_in_background()
def refresh_if_needed(self) -> None: def refresh_if_needed(self) -> None:
if self._refresh_needed: if self._refresh_needed:
self.refresh() self.refresh()