Treker/main.py
2026-04-18 15:53:27 +03:00

449 lines
20 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.

import calendar
from datetime import date
import tkinter as tk
from tkinter import ttk
from core import HarmCalculator
bg = "#eef6ff"
surf = "#ffffff"
surf_alt = "#f6fbff"
head = "#dcebff"
acc = "#4f8df7"
acc_dark = "#3e73cc"
acc_soft = "#d6e9ff"
txt = "#15314a"
muted = "#6b86a1"
bord = "#c8dcf4"
harm_col = "#ff6b6b"
def_habits = [
{"kind": "Алкоголь", "title": "Вечер", "qty": 2, "unit": "бутылка (500мл)", "icon": "🍷", "accent": "#f0a2bc"},
{"kind": "Никотин", "title": "Перерыв", "qty": 4, "unit": "сигарета", "icon": "🚬", "accent": "#8fb0d9"},
{"kind": "Кофе", "title": "Утренний кофе", "qty": 1, "unit": "чашка (200мл)", "icon": "", "accent": "#c8a07a"},
]
class MainWindow(tk.Tk):
def __init__(self):
super().__init__()
self.title("Трекер вредных привычек")
self.geometry("430x780")
self.minsize(370, 700)
self.configure(bg=bg)
self.calc = HarmCalculator(gender="Мужской", age=30)
self.p_name = "Пользователь"
self.p_gender = "Мужской"
self.p_age = 30
self.sett_win = None
self.cal_win = None
self.hab_win = None
self.edit_idx = None
self.cur_date = date.today().replace(day=1)
self.habits = list(def_habits)
self._styles()
self._ui()
self._upd_timer()
def _styles(self):
self.style = ttk.Style(self)
self.style.theme_use("clam")
self.style.configure("Light.Vertical.TScrollbar", troughcolor=bg, background="#9ec5fb", bordercolor=bg, arrowcolor=acc, relief="flat", width=7)
self.style.configure("Light.TCombobox", fieldbackground=surf, foreground=txt, arrowcolor=acc, bordercolor=bord, padding=6)
def _ui(self):
root = tk.Frame(self, bg=bg)
root.pack(fill="both", expand=True)
self._top_bar(root)
self._center(root)
self._list(root)
def _circle(self, parent, text, cmd, size=36, fill=acc, outline=acc):
c = tk.Canvas(parent, width=size, height=size, bg=parent.cget("bg"), highlightthickness=0, cursor="hand2")
def draw(_=None):
c.delete("all")
c.create_oval(1, 1, size-1, size-1, fill=fill, outline=outline)
c.create_text(size/2, size/2, text=text, fill="white", font=("Helvetica", 14, "bold"))
c.bind("<Configure>", draw)
draw()
c.bind("<Button-1>", lambda e: cmd())
return c
def _top_bar(self, parent):
bar = tk.Frame(parent, bg=head, padx=18, pady=16)
bar.pack(fill="x")
self.p_label = tk.Label(bar, text=self.p_name, bg=head, fg=txt, font=("Helvetica", 16, "bold"))
self.p_label.pack(side="left")
btns = tk.Frame(bar, bg=head)
btns.pack(side="right")
self._circle(btns, "🗓", self._cal, 36).pack(side="left", padx=(0, 10))
self._circle(btns, "", self._sett, 36).pack(side="left")
def _center(self, parent):
c = tk.Frame(parent, bg=bg, pady=14)
c.pack(fill="x")
w = tk.Frame(c, bg=bg)
w.pack(pady=8)
self.circ = tk.Canvas(w, width=200, height=200, bg=bg, highlightthickness=0)
self.circ.pack()
self._draw_circ(self.circ, 100, 100, 72)
self.circ.bind("<Button-1>", lambda e: self._open_form())
self.timer = tk.Label(c, text="00:00:00", bg=bg, fg=txt, font=("Helvetica", 28, "bold"))
self.timer.pack(pady=2)
tk.Label(c, text="потеряно за сегодня", bg=bg, fg=muted, font=("Helvetica", 10)).pack(pady=6)
def _list(self, parent):
s = tk.Frame(parent, bg=bg, padx=18, pady=10)
s.pack(fill="both", expand=True)
tk.Label(s, text="Сегодня Вы употребляли", bg=bg, fg=txt, font=("Helvetica", 14, "bold"), anchor="w").pack(fill="x", pady=(0, 12))
w = tk.Frame(s, bg=bg)
w.pack(fill="both", expand=True)
self.canv = tk.Canvas(w, bg=bg, highlightthickness=0)
scr = ttk.Scrollbar(w, orient="vertical", command=self.canv.yview, style="Light.Vertical.TScrollbar")
self.canv.configure(yscrollcommand=scr.set)
scr.pack(side="right", fill="y")
self.canv.pack(side="left", fill="both", expand=True)
self.inner = tk.Frame(self.canv, bg=bg)
self.canv.create_window((0, 0), window=self.inner, anchor="nw")
self.inner.bind("<Configure>", lambda e: self.canv.configure(scrollregion=self.canv.bbox("all")))
self.canv.bind("<Configure>", lambda e: self.canv.itemconfig(self.canv.find_withtag("win")[0], width=e.width) if self.canv.find_withtag("win") else None)
self.canv.create_window((0, 0), window=self.inner, anchor="nw", tags="win")
self._render()
def _render(self):
for w in self.inner.winfo_children():
w.destroy()
for i, h in enumerate(self.habits):
self._card(self.inner, h, i)
def _card(self, parent, habit, idx):
card = tk.Canvas(parent, height=78, bg=bg, highlightthickness=0)
card.pack(fill="x", pady=7)
card.bind("<Configure>", lambda e, c=card: self._draw_card(c, e.width, habit))
self._draw_card(card, 380, habit)
card.bind("<Button-1>", lambda e, i=idx: self._open_form(i))
def _draw_card(self, c, w, h):
c.delete("all")
self._round(c, 0, 0, w, 78, 20, fill=surf, outline=bord)
c.create_oval(18, 15, 66, 63, fill=h["accent"], outline="")
c.create_text(42, 39, text=h["icon"], fill="white", font=("Helvetica", 17, "bold"))
c.create_text(78, 28, anchor="w", text=h["title"], fill=txt, font=("Helvetica", 12, "bold"))
c.create_text(78, 50, anchor="w", text=h["kind"], fill=muted, font=("Helvetica", 10))
c.create_text(w-18, 39, anchor="e", text=f'{h["qty"]} {h["unit"]}', fill=acc_dark, font=("Helvetica", 12, "bold"))
def _round(self, c, x1, y1, x2, y2, r, **kw):
pts = [x1+r, y1, x2-r, y1, x2, y1, x2, y1+r, x2, y2-r, x2, y2, x2-r, y2, x1+r, y2, x1, y2, x1, y2-r, x1, y1+r, x1, y1]
c.create_polygon(pts, smooth=True, splinesteps=24, **kw)
def _draw_circ(self, c, cx, cy, r):
c.create_oval(cx-r-9, cy-r-9, cx+r+9, cy+r+9, fill="#dcecff", outline="")
c.create_oval(cx-r, cy-r, cx+r, cy+r, fill="white", outline=bord, width=2)
c.create_oval(cx-r+12, cy-r+12, cx-r+34, cy-r+34, fill=acc_soft, outline="")
c.create_text(cx, cy, text="+", fill=acc, font=("Helvetica", 26, "bold"))
def _upd_timer(self):
total = sum(self.calc.calc_one(h["kind"], h["unit"], h["qty"]) for h in self.habits)
self.timer.config(text=self.calc.fmt_timer(total))
def _day_stat(self):
total = sum(self.calc.calc_one(h["kind"], h["unit"], h["qty"]) for h in self.habits)
return total, len(self.habits)
def _month_stat(self):
d, c = self._day_stat()
return d * 30, c * 30
def _year_stat(self):
d, c = self._day_stat()
return d * 365, c * 365
def _open_form(self, idx=None):
if self.hab_win and self.hab_win.winfo_exists():
self.hab_win.destroy()
self.edit_idx = idx
data = self.habits[idx] if idx is not None else None
win = tk.Toplevel(self)
win.title("Привычка" if idx is None else "Редактировать")
win.configure(bg=bg)
win.geometry("450x520")
win.resizable(False, False)
win.transient(self)
win.grab_set()
self.hab_win = win
c = tk.Frame(win, bg=bg, padx=18, pady=18)
c.pack(fill="both", expand=True)
tk.Label(c, text="Привычка" if idx is None else "Редактировать", bg=bg, fg=txt, font=("Helvetica", 16, "bold")).pack(anchor="w", pady=(0, 16))
tk.Label(c, text="Тип", bg=bg, fg=muted, font=("Helvetica", 10, "bold")).pack(anchor="w")
self.kind_var = tk.StringVar(value=data["kind"] if data else "Никотин")
kind_cb = ttk.Combobox(c, textvariable=self.kind_var, values=list(self.calc.harms.keys()), state="readonly", style="Light.TCombobox")
kind_cb.pack(fill="x", ipady=4, pady=(0, 12))
kind_cb.bind("<<ComboboxSelected>>", self._kind_chg)
tk.Label(c, text="Единица", bg=bg, fg=muted, font=("Helvetica", 10, "bold")).pack(anchor="w")
self.unit_var = tk.StringVar()
self.unit_cb = ttk.Combobox(c, textvariable=self.unit_var, state="readonly", style="Light.TCombobox")
self.unit_cb.pack(fill="x", ipady=4, pady=(0, 12))
self.title_en = self._entry(c, "Название", data["title"] if data else "")
self.qty_en = self._entry(c, "Количество", str(data["qty"]) if data else "1")
self.qty_en.bind("<KeyRelease>", lambda e: self._prev())
self.prev = tk.Label(c, text="", bg=bg, fg=harm_col, font=("Helvetica", 10, "bold"))
self.prev.pack(anchor="w", pady=6)
self.err = tk.Label(c, text="", bg=bg, fg="#ef6f8a", font=("Helvetica", 10))
self.err.pack(anchor="w", pady=(10, 10))
self._upd_units()
if data:
self.unit_var.set(data["unit"])
self._prev()
if idx is None:
tk.Button(c, text="Добавить", command=self._save, bg=acc, fg="white", font=("Helvetica", 12, "bold"), relief="flat", padx=20, pady=10).pack(fill="x")
else:
row = tk.Frame(c, bg=bg)
row.pack(fill="x")
self._circle(row, "", self._save, 42, acc).pack(side="left")
self._circle(row, "", self._del, 42, "#ef6f8a").pack(side="right")
def _entry(self, parent, label, init):
f = tk.Frame(parent, bg=bg)
f.pack(fill="x", pady=(0, 12))
tk.Label(f, text=label, bg=bg, fg=muted, font=("Helvetica", 10, "bold")).pack(anchor="w")
e = tk.Entry(f, bg=surf, fg=txt, relief="flat", font=("Helvetica", 11), highlightthickness=1, highlightbackground=bord, highlightcolor=acc)
e.pack(fill="x", ipady=8)
e.insert(0, init)
return e
def _upd_units(self):
units = self.calc.get_units(self.kind_var.get())
self.unit_cb['values'] = units
if units:
self.unit_var.set(units[0])
def _kind_chg(self, e=None):
self._upd_units()
self._prev()
def _prev(self):
try:
q = float(self.qty_en.get().strip() if self.qty_en else "1")
k = self.kind_var.get()
u = self.unit_var.get()
if k and u:
h = self.calc.calc_one(k, u, q)
self.prev.config(text=f"⚠️ Потеря: {self.calc.fmt_time(h)} жизни")
except:
self.prev.config(text="⚠️ Введите число")
def _save(self):
k = self.kind_var.get()
u = self.unit_var.get()
t = self.title_en.get().strip()
try:
q = float(self.qty_en.get().strip())
except:
self.err.config(text="Количество — число.")
return
if not t:
t = k
cfg = self.calc.get_config(k)
self.err.config(text="")
pay = {"kind": k, "title": t, "qty": q, "unit": u, "icon": cfg["icon"], "accent": cfg["color"]}
if self.edit_idx is None:
self.habits.append(pay)
else:
self.habits[self.edit_idx] = pay
self._render()
self._upd_timer()
self._close()
h = self.calc.calc_one(k, u, q)
self._notify(k, q, u, h)
def _notify(self, k, q, u, h):
pop = tk.Toplevel(self)
pop.title("⚠️ Вред")
pop.configure(bg="#fff3e0")
pop.geometry("350x160")
pop.resizable(False, False)
pop.transient(self)
pop.update_idletasks()
x = self.winfo_x() + (self.winfo_width() - 350) // 2
y = self.winfo_y() + (self.winfo_height() - 160) // 2
pop.geometry(f"+{x}+{y}")
ic = self.calc.icons.get(k, "⚠️")
tk.Label(pop, text=f"{ic} {k}", font=("Helvetica", 14, "bold"), bg="#fff3e0", fg="#e65100").pack(pady=(20, 5))
tk.Label(pop, text=f"{q} {u}", font=("Helvetica", 12), bg="#fff3e0", fg=txt).pack()
tk.Label(pop, text=f"Потеряно: {self.calc.fmt_time(h)} жизни", font=("Helvetica", 12, "bold"), bg="#fff3e0", fg=harm_col).pack(pady=(10, 0))
pop.after(3000, pop.destroy)
def _del(self):
if self.edit_idx is not None and 0 <= self.edit_idx < len(self.habits):
del self.habits[self.edit_idx]
self._render()
self._upd_timer()
self._close()
def _close(self):
if self.hab_win:
self.hab_win.destroy()
self.hab_win = None
self.edit_idx = None
def _sett(self):
if self.sett_win and self.sett_win.winfo_exists():
self.sett_win.focus_set()
return
win = tk.Toplevel(self)
win.title("Настройки")
win.configure(bg=bg)
win.geometry("400x380")
win.resizable(False, False)
win.transient(self)
win.grab_set()
self.sett_win = win
c = tk.Frame(win, bg=bg, padx=18, pady=18)
c.pack(fill="both", expand=True)
tk.Label(c, text="Профиль", bg=bg, fg=txt, font=("Helvetica", 16, "bold")).pack(anchor="w", pady=(0, 16))
self.name_en = self._entry(c, "Имя", "" if self.p_name == "Пользователь" else self.p_name)
tk.Label(c, text="Пол", bg=bg, fg=muted, font=("Helvetica", 10, "bold")).pack(anchor="w")
self.gen_var = tk.StringVar(value=self.p_gender)
ttk.Combobox(c, textvariable=self.gen_var, values=["Мужской", "Женский"], state="readonly", style="Light.TCombobox").pack(fill="x", ipady=4, pady=(0, 12))
self.age_en = self._entry(c, "Возраст", str(self.p_age))
tk.Button(c, text="Сохранить", command=self._save_prof, bg=acc, fg="white", font=("Helvetica", 12, "bold"), relief="flat", padx=20, pady=10).pack(fill="x", pady=(8, 0))
def _save_prof(self):
n = self.name_en.get().strip()
self.p_name = n or "Пользователь"
self.p_gender = self.gen_var.get()
try:
self.p_age = int(self.age_en.get().strip())
except:
self.p_age = 20
self.calc.set_profile(self.p_gender, self.p_age)
self.p_label.config(text=self.p_name)
self._upd_timer()
if self.sett_win:
self.sett_win.destroy()
self.sett_win = None
def _cal_title(self):
m = ["Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь"]
return f"{m[self.cur_date.month - 1]} {self.cur_date.year}"
def _cal(self):
if self.cal_win and self.cal_win.winfo_exists():
self.cal_win.focus_set()
return
win = tk.Toplevel(self)
win.title("Календарь")
win.configure(bg=bg)
win.geometry("460x600")
win.resizable(False, False)
win.transient(self)
win.grab_set()
self.cal_win = win
h = tk.Frame(win, bg=head, padx=18, pady=18)
h.pack(fill="x")
nav = tk.Frame(h, bg=head)
nav.pack(fill="x")
self._circle(nav, "", self._prev_m, 30).pack(side="left")
self.cal_lbl = tk.Label(nav, text=self._cal_title(), bg=head, fg=txt, font=("Helvetica", 14, "bold"))
self.cal_lbl.pack(side="left", expand=True)
self._circle(nav, "", self._next_m, 30).pack(side="right")
body = tk.Frame(win, bg=bg, padx=18, pady=4)
body.pack(fill="both", expand=True)
self.grid = tk.Frame(body, bg=surf)
self.grid.pack(fill="x", pady=(0, 10))
sf = tk.Frame(body, bg=surf)
sf.pack(fill="x", pady=5)
si = tk.Frame(sf, bg=surf, padx=12, pady=12)
si.pack(fill="x")
tk.Label(si, text="📊 Статистика", bg=surf, fg=txt, font=("Helvetica", 12, "bold"), anchor="w").pack(fill="x", pady=(0, 8))
self.day_lbl = tk.Label(si, text="", bg=surf, fg=muted, font=("Helvetica", 10), anchor="w")
self.day_lbl.pack(fill="x")
self.mon_lbl = tk.Label(si, text="", bg=surf, fg=muted, font=("Helvetica", 10), anchor="w")
self.mon_lbl.pack(fill="x")
self.yr_lbl = tk.Label(si, text="", bg=surf, fg=muted, font=("Helvetica", 10), anchor="w")
self.yr_lbl.pack(fill="x")
self.cht = tk.Text(body, bg=surf, fg=txt, font=("Courier", 9), height=10, relief="flat", borderwidth=0)
self.cht.pack(fill="both", expand=True, pady=5)
self._refresh()
def _upd_period(self):
d, dc = self._day_stat()
m, mc = self._month_stat()
y, yc = self._year_stat()
self.day_lbl.config(text=f"📅 Сегодня: {self.calc.fmt_time(d)} ({dc} пр.)")
self.mon_lbl.config(text=f"📆 Месяц: {self.calc.fmt_time(m)} (×30)")
self.yr_lbl.config(text=f"📈 Год: {self.calc.fmt_time(y)} (×365)")
def _draw_cht(self):
self.cht.delete("1.0", tk.END)
self.cht.insert(tk.END, f"{self._cal_title()}:\n\n")
d, _ = self._day_stat()
bar = "" * min(30, int(d/10)) + "" * (30 - min(30, int(d/10)))
self.cht.insert(tk.END, f"Сегодня: {bar} {self.calc.fmt_time(d)}\n")
def _refresh(self):
if not self.cal_win: return
self.cal_lbl.config(text=self._cal_title())
self._draw_grid(self.grid, self.cur_date.year, self.cur_date.month)
self._upd_period()
self._draw_cht()
def _draw_grid(self, p, y, m):
for w in p.winfo_children():
w.destroy()
today = date.today()
dm, _ = self._day_stat()
for col, day in enumerate(["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"]):
tk.Label(p, text=day, bg=surf, fg=muted, font=("Helvetica", 10, "bold"), width=4, pady=6).grid(row=0, column=col, sticky="nsew")
for r, week in enumerate(calendar.monthcalendar(y, m), 1):
for c, day in enumerate(week):
if day == 0:
cell = tk.Label(p, text="", bg=surf, width=4, height=2)
else:
is_t = (today.year == y and today.month == m and today.day == day)
if is_t and dm > 0:
if dm > 120: bg_c, fg_c = "#ff4444", "white"
elif dm > 60: bg_c, fg_c = "#ff7777", "white"
else: bg_c, fg_c = "#ffaaaa", txt
elif is_t:
bg_c, fg_c = acc, "white"
else:
bg_c, fg_c = surf_alt, txt
cell = tk.Label(p, text=str(day), bg=bg_c, fg=fg_c, font=("Helvetica", 10, "bold"), width=4, height=2)
cell.grid(row=r, column=c, padx=3, pady=3, sticky="nsew")
for i in range(7):
p.grid_columnconfigure(i, weight=1)
def _prev_m(self):
y, m = self.cur_date.year, self.cur_date.month - 1
if m == 0: m, y = 12, y - 1
self.cur_date = date(y, m, 1)
self._refresh()
def _next_m(self):
y, m = self.cur_date.year, self.cur_date.month + 1
if m == 13: m, y = 1, y + 1
self.cur_date = date(y, m, 1)
self._refresh()
if __name__ == "__main__":
app = MainWindow()
app.mainloop()