449 lines
20 KiB
Python
449 lines
20 KiB
Python
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() |