diff --git a/main b/main new file mode 100644 index 0000000..54cb12e --- /dev/null +++ b/main @@ -0,0 +1,449 @@ +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() \ No newline at end of file