600 lines
No EOL
26 KiB
Python
600 lines
No EOL
26 KiB
Python
# 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() |