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