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("", draw) draw() c.bind("", 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("", 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("", lambda e: self.canv.configure(scrollregion=self.canv.bbox("all"))) self.canv.bind("", 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("", lambda e, c=card: self._draw_card(c, e.width, habit)) self._draw_card(card, 380, habit) card.bind("", 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("<>", 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("", 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()