Motivationstracker erstellt
This commit is contained in:
commit
1d5473fd4f
1 changed files with 600 additions and 0 deletions
600
motivations_app_v1_2.py
Normal file
600
motivations_app_v1_2.py
Normal file
|
|
@ -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("<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()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue