diff --git a/__init__.py b/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/anki_helpers/__init__.py b/anki_helpers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/anki_helpers/activity.py b/anki_helpers/activity.py new file mode 100644 index 000000000..85c4e0e0c --- /dev/null +++ b/anki_helpers/activity.py @@ -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, + } diff --git a/pylib/anki/stats.py b/pylib/anki/stats.py index 2f7de2e04..eb20ea646 100644 --- a/pylib/anki/stats.py +++ b/pylib/anki/stats.py @@ -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 += "
" 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 += "
Activity (last 30 days):
" + b += f"Active days: {activity['active_days']}
" + b += f"Average/day: {activity['average_per_day']}
" + b += f"Low activity days: {activity['low_days']}
" + b += "
TEST123 - wird dieser Text angezeigt?
" + b += "

TEST123 sichtbar?


" + b += "
Activity (last 30 days):
" + b += "Active days: 10
" + b += "Average/day: 5.5
" + b += "Low activity days: 3
" + + 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""" "%(pct)d%% (%(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 ( - """ -
""" - % self.width - + "".join(i) - + "
" + """ +
""" + % self.width + + "".join(i) + + "
" ) 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 = {} diff --git a/qt/aqt/overview.py b/qt/aqt/overview.py index da017714f..812a191a3 100644 --- a/qt/aqt/overview.py +++ b/qt/aqt/overview.py @@ -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()