Motivationstracker/motivations_app_v1_2.py
2026-01-16 22:19:24 +01:00

600 lines
No EOL
26 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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("<MouseWheel>", _on_mousewheel)
canvas.bind_all("<Button-4>", _on_mousewheel)
canvas.bind_all("<Button-5>", _on_mousewheel)
def _unbound_to_mousewheel(event):
canvas.unbind_all("<MouseWheel>")
canvas.unbind_all("<Button-4>")
canvas.unbind_all("<Button-5>")
canvas.bind('<Enter>', _bound_to_mousewheel)
canvas.bind('<Leave>', _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("<Configure>", 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("<Configure>", 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("<Configure>", 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()