commit 1d5473fd4f7197d2fa65a735003086fdeb8f1c59 Author: tea Date: Fri Jan 16 22:19:24 2026 +0100 Motivationstracker erstellt diff --git a/motivations_app_v1_2.py b/motivations_app_v1_2.py new file mode 100644 index 0000000..782341a --- /dev/null +++ b/motivations_app_v1_2.py @@ -0,0 +1,600 @@ +# tracker_v1_2.py +# Motivation Tracker, non-threatening UI für gute Laune +# Eingebaute intermittierende Verstärkung (Zufallsgewinnausschüttung) für mehr Motivation +# Scrollable, Matplotlib stats, JSON persistence +# Python 3.8+ + + +import json +import os +import random +from datetime import datetime, timedelta +import tkinter as tk +from tkinter import ttk, messagebox, simpledialog + +# Matplotlib (TkAgg backend) +import matplotlib +matplotlib.use("TkAgg") +from matplotlib.figure import Figure +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg +import matplotlib.dates as mdates +import matplotlib.font_manager as fm +from matplotlib import rcParams + +# ------------------------- +# Config +# ------------------------- +DATA_FILE = "data_v1_1.json" +PREFERRED_FONT = "Segoe UI" # falls nicht vorhanden, Tk ersetzt automatisch + +# default tasks and kaomoji +DEFAULT_TASKS = ["CTF", "HTB", "PYTHON", "DIGITAL GARDENING"] +KAOMOJI = { + "CTF": " ✧(๑ↀᆺↀ๑)✧", + "HTB": " 🌙(˘͈ᵕ ˘͈♡)", + "PYTHON": " 🜁(Φ ω Φ)", + "DIGITAL GARDENING": " 🌿(╹‿╹✿)" +} +DEFAULT_KAO = " ✨(˘͈ᵕ˘͈)✨" + +# color theme: +COLORS = { + "bg": "#FBF7FF", + "header": "#EADCF6", + "card": "#FFF4F9", + "muted": "#7B6699", + "accent": "#6B3FA0", + "dark_accent": "#34224A", + "text": "#2E2440", + "positive": "#2E7D32", + "bar1": "#A56CC1", + "bar2": "#4B2C5E" +} + +rcParams["font.family"] = [ + "Segoe UI Symbol", + "DejaVu Sans" +] +rcParams["figure.dpi"] = 120 +rcParams["savefig.dpi"] = 120 + +# ------------------------- +# Data helpers +# ------------------------- +def load_data(): + if os.path.exists(DATA_FILE): + try: + with open(DATA_FILE, "r", encoding="utf-8") as f: + data = json.load(f) + except Exception: + data = {} + else: + data = {} + data.setdefault("tasks", DEFAULT_TASKS.copy()) + data.setdefault("history", {}) # {"YYYY-MM-DD": {task: bool, ...}} + data.setdefault("current_week_points", 0) + data.setdefault("total_earned", 0.0) + data.setdefault("last_payout_date", None) + data.setdefault("payout_history", []) + + # Listen wieder in Sets umwandeln (für rewarded_today) + if "rewarded_today" in data: + data["rewarded_today"] = {k: set(v) for k, v in data["rewarded_today"].items()} + + return data + +def save_data(data): + try: + # Sets in Listen umwandeln für JSON + data_copy = data.copy() + if "rewarded_today" in data_copy: + data_copy["rewarded_today"] = {k: list(v) for k, v in data_copy["rewarded_today"].items()} + + with open(DATA_FILE, "w", encoding="utf-8") as f: + json.dump(data_copy, f, indent=2, ensure_ascii=False) + except Exception as e: + print("Fehler beim Speichern:", e) + +# ------------------------- +# Utility: OS-independent mousewheel binding for canvas +# ------------------------- +def bind_canvas_mousewheel(canvas): + def _on_mousewheel(event): + # event.delta on Windows with Tk returns multiples of 120 + if event.num == 5 or event.delta < 0: + canvas.yview_scroll(1, "units") + elif event.num == 4 or event.delta > 0: + canvas.yview_scroll(-1, "units") + + # Bind/Unbind nur wenn Maus über Canvas ist + def _bound_to_mousewheel(event): + canvas.bind_all("", _on_mousewheel) + canvas.bind_all("", _on_mousewheel) + canvas.bind_all("", _on_mousewheel) + + def _unbound_to_mousewheel(event): + canvas.unbind_all("") + canvas.unbind_all("") + canvas.unbind_all("") + + canvas.bind('', _bound_to_mousewheel) + canvas.bind('', _unbound_to_mousewheel) + +# ------------------------- +# Main App +# ------------------------- +class TrackerApp: + def __init__(self, root): + self.root = root + self.root.title("🔮 Tracker v1.2") + self.root.geometry("900x500") + self.root.minsize(820, 600) + + self.data = load_data() + self._ensure_today() + self._build_ui() + # optional: auto check weekly payout on startup if saturday and not paid + self._check_auto_payout_on_startup() + + def _ensure_today(self): + today = datetime.now().strftime("%Y-%m-%d") + tasks = self.data.get("tasks", DEFAULT_TASKS.copy()) + if today not in self.data["history"]: + self.data["history"][today] = {t: False for t in tasks} + save_data(self.data) + # sync for changed tasks + for _, rec in self.data["history"].items(): + for t in self.data["tasks"]: + if t not in rec: + rec[t] = False + + # ------------------------- + # Build UI (with Scrollbar) + # ------------------------- + def _build_ui(self): + # Header + header = tk.Frame(self.root, bg=COLORS["header"], pady=10) + header.pack(fill=tk.X) + tk.Label(header, text="(っ◉ω◉)っ~~☆'。゚.✧.・。゚★'.・.・。゚", bg=COLORS["header"], + fg=COLORS["dark_accent"], font=(PREFERRED_FONT, 18, "bold")).pack() + tk.Label(header, text="Kleine Rituale, viel Magie ✨", + bg=COLORS["header"], fg=COLORS["muted"], font=(PREFERRED_FONT, 10)).pack() + + # Scrollable main area: canvas + vertical scrollbar + container = tk.Frame(self.root, bg=COLORS["bg"]) + container.pack(fill=tk.BOTH, expand=True, padx=10, pady=8) + + self.canvas = tk.Canvas(container, bg=COLORS["bg"], highlightthickness=0) + self.v_scroll = ttk.Scrollbar(container, orient="vertical", command=self.canvas.yview) + self.canvas.configure(yscrollcommand=self.v_scroll.set) + + self.v_scroll.pack(side=tk.RIGHT, fill=tk.Y) + self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + + # inner frame (where content goes) + self.main_frame = tk.Frame(self.canvas, bg=COLORS["bg"]) + self.canvas.create_window((0, 0), window=self.main_frame, anchor="nw") + + # bind configure to update scrollregion + self.main_frame.bind("", lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all"))) + + # bind mousewheel / trackpad scrolling + bind_canvas_mousewheel(self.canvas) + + # --- add content to main_frame --- + self._build_content_cards() + + def _build_content_cards(self): + # week summary card + week_card = tk.Frame(self.main_frame, bg=COLORS["card"], bd=1, relief=tk.RIDGE) + week_card.pack(fill=tk.X, pady=(0, 10)) + tk.Label(week_card, text="🌙 Diese Woche gesammelt:", bg=COLORS["card"], fg=COLORS["muted"], + font=(PREFERRED_FONT, 12, "bold")).pack(anchor="w", padx=12, pady=(8, 0)) + self.week_label = tk.Label(week_card, text=self._week_text(), bg=COLORS["card"], + fg=COLORS["accent"], font=(PREFERRED_FONT, 16, "bold")) + self.week_label.pack(anchor="w", padx=12, pady=(0, 12)) + + # tasks section + tasks_card = tk.LabelFrame(self.main_frame, text=f"📅 Heute: {datetime.now().strftime('%d.%m.%Y')}", + bg=COLORS["bg"], fg=COLORS["text"], font=(PREFERRED_FONT, 12, "bold")) + tasks_card.pack(fill=tk.BOTH, pady=(0, 10)) + + self.task_vars = {} + color_seq = [COLORS["card"], "#FFF6EA", "#E9F7F2", "#F7EAF4"] + today_key = datetime.now().strftime("%Y-%m-%d") + for i, t in enumerate(self.data.get("tasks", DEFAULT_TASKS)): + bg = color_seq[i % len(color_seq)] + row = tk.Frame(tasks_card, bg=bg, bd=1, relief=tk.GROOVE) + row.pack(fill=tk.X, padx=8, pady=6) + var = tk.BooleanVar(value=self.data["history"].get(today_key, {}).get(t, False)) + self.task_vars[t] = var + ka = KAOMOJI.get(t, DEFAULT_KAO) + cb_text = f" ✦ {t} ✦{ka}" + cb = tk.Checkbutton(row, text=cb_text, variable=var, bg=bg, fg=COLORS["text"], + font=(PREFERRED_FONT, 12), anchor="w", + command=lambda task=t: self._toggle_task(task)) + cb.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=6, pady=8) + points = tk.Label(row, text="→ 20 Cent ✨", bg=bg, fg=COLORS["bar2"], font=(PREFERRED_FONT, 10, "bold")) + points.pack(side=tk.RIGHT, padx=8) + + # progress card + prog_card = tk.Frame(self.main_frame, bg=COLORS["card"], bd=1, relief=tk.RIDGE) + prog_card.pack(fill=tk.X, pady=(0, 10)) + tk.Label(prog_card, text="🔮 Heutiger Fortschritt:", bg=COLORS["card"], fg=COLORS["muted"], + font=(PREFERRED_FONT, 12, "bold")).pack(anchor="w", padx=12, pady=(8, 6)) + style = ttk.Style() + style.configure("Horizontal.TProgressbar", troughcolor=COLORS["bg"], background=COLORS["accent"]) + self.progress = ttk.Progressbar(prog_card, style="Horizontal.TProgressbar", mode="determinate") + self.progress.pack(fill=tk.X, padx=12, pady=(0, 8)) + self.progress_label = tk.Label(prog_card, text="", bg=COLORS["card"], fg=COLORS["text"], font=(PREFERRED_FONT, 11)) + self.progress_label.pack(anchor="w", padx=12, pady=(0,8)) + + # bottom controls area + bottom = tk.Frame(self.main_frame, bg=COLORS["bg"]) + bottom.pack(fill=tk.X, pady=(0,12)) + + total_card = tk.Frame(bottom, bg=COLORS["card"], bd=1, relief=tk.RIDGE) + total_card.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0,8)) + tk.Label(total_card, text="🌿 Gesamt verdient:", bg=COLORS["card"], fg=COLORS["muted"], + font=(PREFERRED_FONT, 12, "bold")).pack(anchor="w", padx=10, pady=(8,2)) + self.total_label = tk.Label(total_card, text=f"{self.data.get('total_earned',0.0):.2f} €", + bg=COLORS["card"], fg=COLORS["positive"], font=(PREFERRED_FONT, 14, "bold")) + self.total_label.pack(anchor="w", padx=10, pady=(0,12)) + + controls = tk.Frame(bottom, bg=COLORS["bg"]) + controls.pack(side=tk.RIGHT) + tk.Button(controls, text="📜 Verlauf", command=self.show_history, font=(PREFERRED_FONT, 11), bg=COLORS["card"]).pack(side=tk.LEFT, padx=6) + tk.Button(controls, text="💶 Auszahlungen", command=self.show_payout_history, font=(PREFERRED_FONT, 11), bg=COLORS["card"]).pack(side=tk.LEFT, padx=6) + tk.Button(controls, text="📈 Statistiken", command=self.show_stats, font=(PREFERRED_FONT, 11), bg=COLORS["card"]).pack(side=tk.LEFT, padx=6) + + # task management (add/remove/reset) + manage = tk.Frame(self.main_frame, bg=COLORS["bg"]) + manage.pack(fill=tk.X, pady=(6,12)) + self.new_task_entry = tk.Entry(manage, font=(PREFERRED_FONT, 12)) + self.new_task_entry.pack(side=tk.LEFT, padx=(6,6)) + tk.Button(manage, text="+ Aufgabe hinzufügen", command=self.add_task, font=(PREFERRED_FONT, 11)).pack(side=tk.LEFT, padx=6) + tk.Button(manage, text="− Aufgabe entfernen", command=self.remove_task, font=(PREFERRED_FONT, 11)).pack(side=tk.LEFT, padx=6) + tk.Button(manage, text="♻️ Tag zurücksetzen", command=self.reset_today, font=(PREFERRED_FONT, 11)).pack(side=tk.LEFT, padx=6) + + # final update of displays + self.update_display() + + # ------------------------- + # Toggle logic for tasks + # FIXED: Verhindert Punkt-Exploit durch rewarded_today Tracking + # ------------------------- + def _toggle_task(self, task): + today = datetime.now().strftime("%Y-%m-%d") + self.data.setdefault("history", {}) + self.data["history"].setdefault(today, {t: False for t in self.data.get("tasks", DEFAULT_TASKS)}) + + # Neu: Tracking für bereits belohnte Tasks + self.data.setdefault("rewarded_today", {}) + if today not in self.data["rewarded_today"]: + self.data["rewarded_today"][today] = set() + + was = self.data["history"][today].get(task, False) + nowv = bool(self.task_vars[task].get()) + self.data["history"][today][task] = nowv + + # Nur Punkte vergeben, wenn Task erstmalig heute abgehakt wird + if nowv and not was and task not in self.data["rewarded_today"][today]: + self.data["current_week_points"] = self.data.get("current_week_points",0) + 1 + self.data["rewarded_today"][today].add(task) # Als belohnt markieren + + # Tagesbonus nur wenn alle tasks abgehakt und noch nicht alle belohnt wurden + if all(self.data["history"][today].values()): + # Prüfen ob Tagesbonus heute schon vergeben wurde + if not self.data.get("daily_bonus_given", {}).get(today, False): + self.data["current_week_points"] += 2 + self.data.setdefault("daily_bonus_given", {})[today] = True + messagebox.showinfo("🌟 Tagesbonus", "Alle Aufgaben erledigt! +2 Bonuspunkte ✨") + + # Random surprise nur beim ersten Abhaken + if random.random() < 0.12: + add = random.randint(1, 3) + self.data["current_week_points"] += add + messagebox.showinfo("🔮 Überraschung", f"Du hast {add} Bonuspunkte gewonnen! {DEFAULT_KAO}") + + elif not nowv and was: + # Punkte werden nicht abgezogen, da die Belohnung schon vergeben wurde + pass + + save_data(self.data) + self.update_display() + + # ------------------------- + # Add / Remove Tasks + # ------------------------- + def add_task(self): + t = self.new_task_entry.get().strip() + if not t: + return + if t in self.data.get("tasks", []): + messagebox.showwarning("Schon vorhanden", "Diese Aufgabe existiert bereits.") + return + self.data["tasks"].append(t) + if t not in KAOMOJI: + KAOMOJI[t] = DEFAULT_KAO + # add to all existing days + for _, rec in self.data["history"].items(): + rec[t] = False + save_data(self.data) + messagebox.showinfo("Hinzugefügt", f"Aufgabe '{t}' wurde hinzugefügt. {KAOMOJI[t]}") + self._rebuild_ui() # rebuild so new checkbox appears + + def remove_task(self): + t = simpledialog.askstring("Aufgabe entfernen", "Gib den genauen Namen der Aufgabe ein:") + if not t: + return + if t not in self.data.get("tasks", []): + messagebox.showwarning("Nicht gefunden", "Diese Aufgabe existiert nicht.") + return + if not messagebox.askyesno("Bestätigen", f"Soll '{t}' wirklich entfernt werden?"): + return + self.data["tasks"].remove(t) + for day_key in list(self.data["history"].keys()): + if t in self.data["history"][day_key]: + del self.data["history"][day_key][t] + if t in KAOMOJI: + del KAOMOJI[t] + save_data(self.data) + messagebox.showinfo("Entfernt", f"'{t}' wurde entfernt.") + self._rebuild_ui() + + def reset_today(self): + today = datetime.now().strftime("%Y-%m-%d") + if today in self.data["history"]: + for k in self.data["history"][today]: + self.data["history"][today][k] = False + # Reset auch rewarded_today und daily_bonus + if today in self.data.get("rewarded_today", {}): + self.data["rewarded_today"][today] = set() + if today in self.data.get("daily_bonus_given", {}): + self.data["daily_bonus_given"][today] = False + save_data(self.data) + self._rebuild_ui() + self.update_display() + messagebox.showinfo("Zurückgesetzt", "Heute wurden alle Aufgaben zurückgesetzt.") + else: + messagebox.showinfo("Kein Eintrag", "Heute gibt es keine Einträge zum Zurücksetzen.") + + # ------------------------- + # Rebuild UI (simpler approach) + # ------------------------- + def _rebuild_ui(self): + # destroy and rebuild to refresh tasks list + for w in self.root.winfo_children(): + w.destroy() + # reload data and rebuild + self.data = load_data() + self._ensure_today() + self._build_ui() + + # ------------------------- + # Display updates + # ------------------------- + def _week_text(self): + pts = self.data.get("current_week_points", 0) + euros = pts * 0.20 + return f"{pts} Punkte = {euros:.2f} €" + + def update_display(self): + # update summary labels and progress + self.week_label.config(text=self._week_text()) + self.total_label.config(text=f"{self.data.get('total_earned', 0.0):.2f} €") + today = datetime.now().strftime("%Y-%m-%d") + total = len(self.data.get("tasks", [])) + done = 0 + if today in self.data["history"]: + done = sum(1 for v in self.data["history"][today].values() if v) + percent = int(done / total * 100) if total > 0 else 0 + self.progress['value'] = percent + self.progress_label.config(text=f"{done}/{total} Tasks erledigt") + # sync checkbox variables + for task, var in self.task_vars.items(): + cur = self.data["history"].get(today, {}).get(task, False) + var.set(bool(cur)) + + # ------------------------- + # History & Payout + # ------------------------- + def show_history(self): + win = tk.Toplevel(self.root) + win.title("📜 Verlauf") + win.geometry("640x560") + win.configure(bg=COLORS["bg"]) + tk.Label(win, text="📚 Verlauf (letzte 30 Tage)", bg=COLORS["bg"], fg=COLORS["dark_accent"], + font=(PREFERRED_FONT, 14, "bold")).pack(pady=8) + + cont = tk.Frame(win, bg=COLORS["bg"]) + cont.pack(fill=tk.BOTH, expand=True, padx=8, pady=6) + canvas = tk.Canvas(cont, bg=COLORS["bg"], highlightthickness=0) + vs = ttk.Scrollbar(cont, orient="vertical", command=canvas.yview) + canvas.configure(yscrollcommand=vs.set) + vs.pack(side=tk.RIGHT, fill=tk.Y) + canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + frame = tk.Frame(canvas, bg=COLORS["bg"]) + canvas.create_window((0,0), window=frame, anchor="nw") + frame.bind("", lambda e: canvas.configure(scrollregion=canvas.bbox("all"))) + bind_canvas_mousewheel(canvas) + + days = sorted(self.data.get("history", {}).keys(), reverse=True)[:30] + for d in days: + rec = self.data["history"].get(d, {}) + total = len(rec) + done = sum(1 for v in rec.values() if v) + card = tk.Frame(frame, bg=COLORS["card"], bd=1, relief=tk.RIDGE) + card.pack(fill=tk.X, pady=6, padx=6) + tk.Label(card, text=f"{d} — {done}/{total} erledigt", bg=COLORS["card"], fg=COLORS["accent"], + font=(PREFERRED_FONT, 12, "bold")).pack(anchor="w", padx=8, pady=(6,2)) + comp = [t for t, v in rec.items() if v] + if comp: + tk.Label(card, text="Erledigt: " + ", ".join(comp), bg=COLORS["card"], fg=COLORS["text"], + font=(PREFERRED_FONT, 10)).pack(anchor="w", padx=8, pady=(0,8)) + else: + tk.Label(card, text="Keine Aufgaben erledigt an diesem Tag.", bg=COLORS["card"], fg=COLORS["muted"], + font=(PREFERRED_FONT, 10, "italic")).pack(anchor="w", padx=8, pady=(0,8)) + + def show_payout_history(self): + win = tk.Toplevel(self.root) + win.title("💶 Auszahlungshistorie") + win.geometry("520x420") + win.configure(bg=COLORS["bg"]) + tk.Label(win, text="💰 Auszahlungshistorie", bg=COLORS["bg"], fg=COLORS["accent"], + font=(PREFERRED_FONT, 14, "bold")).pack(pady=8) + ph = list(reversed(self.data.get("payout_history", []))) + if not ph: + tk.Label(win, text="Noch keine Auszahlungen.", bg=COLORS["bg"], fg=COLORS["text"], + font=(PREFERRED_FONT, 11)).pack(pady=18) + return + frame = tk.Frame(win, bg=COLORS["bg"]) + frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=6) + for p in ph: + tk.Label(frame, text=f"{p['date']}: {p['amount']:.2f} € ({p['points']} pts)", + bg=COLORS["card"], fg=COLORS["text"], font=(PREFERRED_FONT, 11)).pack(fill=tk.X, padx=8, pady=6) + + # manual trigger for weekly payout + def trigger_weekly_payout(self): + pts = self.data.get("current_week_points", 0) + if pts <= 0: + messagebox.showinfo("Keine Punkte", "Keine Punkte diese Woche.") + return + amount = round(pts * 0.20, 2) + if messagebox.askyesno("Auszahlung", f"{amount:.2f} € auszahlen?"): + date = datetime.now().strftime("%Y-%m-%d") + self.data.setdefault("payout_history", []).append({"date": date, "points": pts, "amount": amount}) + self.data["total_earned"] = round(self.data.get("total_earned", 0.0) + amount, 2) + self.data["current_week_points"] = 0 + self.data["last_payout_date"] = date + save_data(self.data) + self.update_display() + messagebox.showinfo("Ausgezahlt", f"{amount:.2f} € wurden ausgezahlt. Viel Freude!") + + def _check_auto_payout_on_startup(self): + # if it's Saturday and not paid today, optionally show a non-blocking prompt + today = datetime.now() + if today.weekday() == 5: # Saturday + last = self.data.get("last_payout_date") + today_str = today.strftime("%Y-%m-%d") + if last != today_str and self.data.get("current_week_points", 0) > 0: + # show a small popup to allow payout + if messagebox.askyesno("Samstag", "Heute ist Samstag — Auszahlung der Woche durchführen?"): + self.trigger_weekly_payout() + + # ------------------------- + # Stats (Matplotlib embedded), well padded so labels not cut + # ------------------------- + def show_stats(self): + win = tk.Toplevel(self.root) + win.title("📈 Statistiken") + win.geometry("940x500") + win.configure(bg=COLORS["bg"]) + + tk.Label(win, text="📊 Fortschrittsübersicht", bg=COLORS["bg"], fg=COLORS["accent"], + font=(PREFERRED_FONT, 16, "bold")).pack(pady=10) + + + cont = tk.Frame(win, bg=COLORS["bg"]) + cont.pack(fill=tk.BOTH, expand=True, padx=8, pady=8) + + canvas = tk.Canvas(cont, bg=COLORS["bg"], highlightthickness=0) + vscroll = ttk.Scrollbar(cont, orient="vertical", command=canvas.yview) + canvas.configure(yscrollcommand=vscroll.set) + vscroll.pack(side=tk.RIGHT, fill=tk.Y) + canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + inner = tk.Frame(canvas, bg=COLORS["bg"]) + canvas.create_window((0,0), window=inner, anchor="nw") + inner.bind("", lambda e: canvas.configure(scrollregion=canvas.bbox("all"))) + bind_canvas_mousewheel(canvas) + + + # --- last 7 days line chart + days = [] + counts = [] + for i in range(6, -1, -1): + d = (datetime.now() - timedelta(days=i)).date() + days.append(d) + key = d.strftime("%Y-%m-%d") + counts.append(sum(1 for v in self.data["history"].get(key, {}).values() if v)) + + fig1 = Figure(figsize=(8.6, 3.0), dpi=100) + ax1 = fig1.add_subplot(111) + ax1.plot(days, counts, marker="o", linewidth=2, color=COLORS["bar1"]) + ax1.set_title("Letzte 7 Tage — erledigte Tasks", color=COLORS["accent"], fontsize=12) + ax1.set_ylabel("Tasks") + ax1.set_ylim(0, max(4, max(counts) + 1)) + ax1.grid(axis="y", linestyle="--", alpha=0.25) + ax1.xaxis.set_major_formatter(mdates.DateFormatter("%a\n%d.%m")) + fig1.tight_layout(pad=1.2) + canvas1 = FigureCanvasTkAgg(fig1, master=inner) + canvas1.draw() + canvas1.get_tk_widget().pack(pady=12, fill=tk.BOTH, expand=True) + + # --- last 6 months bar chart + months = [] + mcounts = [] + today = datetime.now().date() + for off in range(5, -1, -1): + y = today.year + m = today.month - off + while m <= 0: + m += 12 + y -= 1 + months.append((y, m)) + # count tasks + c = 0 + for date_str, rec in self.data["history"].items(): + try: + d = datetime.strptime(date_str, "%Y-%m-%d").date() + except Exception: + continue + if d.year == y and d.month == m: + c += sum(1 for v in rec.values() if v) + mcounts.append(c) + + month_labels = [datetime(y, m, 1).strftime("%b %Y") for (y, m) in months] + fig2 = Figure(figsize=(8.6, 3.4), dpi=100) + ax2 = fig2.add_subplot(111) + bars = ax2.bar(month_labels, mcounts, color=COLORS["bar2"], alpha=0.95) + ax2.set_title("Letzte 6 Monate — erledigte Tasks (gesamt)", color=COLORS["accent"], fontsize=12) + ax2.set_ylabel("Tasks gesamt") + ax2.grid(axis="y", linestyle="--", alpha=0.25) + # kaomoji above bars + for rect, val in zip(bars, mcounts): + xpos = rect.get_x() + rect.get_width() / 2 + ypos = rect.get_height() + 0.4 + if val == 0: + k = "🌙(˘˘)" + elif val < 6: + k = "🔮(˘͈ᵕ˘͈)" + elif val < 15: + k = "✨(๑˃ᴗ˂)ﻭ" + else: + k = "🌟\(^▽^)/" + ax2.text(xpos, ypos, k, ha="center", va="bottom", fontsize=10) + fig2.tight_layout(pad=1.3) + canvas2 = FigureCanvasTkAgg(fig2, master=inner) + canvas2.draw() + canvas2.get_tk_widget().pack(pady=12, fill=tk.BOTH, expand=True) + + +# ------------------------- +# Run +# ------------------------- +if __name__ == "__main__": + main_root = tk.Tk() + + # --- HiDPI / schärfere Schrift (Windows) --- + try: + import ctypes + ctypes.windll.shcore.SetProcessDpiAwareness(1) # SYSTEM_AWARE + except Exception: + pass + + # --- Tk-Skalierung an System-DPI anpassen --- + dpi = main_root.winfo_fpixels('1i') + main_root.tk.call("tk", "scaling", dpi / 72) + + app = TrackerApp(main_root) + main_root.mainloop() \ No newline at end of file