diff --git a/break.mp3 b/break.mp3 new file mode 100644 index 0000000..1c347c1 Binary files /dev/null and b/break.mp3 differ diff --git a/default.mp3 b/default.mp3 new file mode 100644 index 0000000..c0914db Binary files /dev/null and b/default.mp3 differ diff --git a/icon.ico b/icon.ico new file mode 100644 index 0000000..219dfc5 Binary files /dev/null and b/icon.ico differ diff --git a/model.py b/model.py new file mode 100644 index 0000000..2260939 --- /dev/null +++ b/model.py @@ -0,0 +1,107 @@ +from icecream import ic +from dataclasses import dataclass +from random import randint +import pygame +import time +import sys +import os + +def resource_path(relative_path): + """ Get absolute path to resource, works for dev and for PyInstaller """ + try: + # PyInstaller creates a temp folder and stores path in _MEIPASS + base_path = sys._MEIPASS + except Exception: + base_path = os.path.abspath(".") + + return os.path.join(base_path, relative_path) + +class Queue(object): + def __init__(self): + self.tasks = [] + self.task_slots = {} + self.task_brake = self.set_break() + self.run_task = False + self.task_slots_max = 6 + self.current_task = None + self.current_task_time = 0 + self.current_task_time_current = 0 + self.current_task_time_max = 0 + pygame.mixer.init() + + def sound(self): + sound = pygame.mixer.Sound(resource_path(self.current_task.sound)) + sound.play() + + def set_break(self): + task_id = -1 + task_name = f"Перерыв 10 минут" + task_count = 0 + task_description = f"Десятиминутный перерыв" + task_time = 0 + task_sound = f"./sounds/break.mp3" + return Task(task_id, task_name, task_count, task_description, task_time, task_sound, False) + + def create_task(self, name, description = '', num = 0): + task_id = num + task_name = name + task_count = 0 + task_description = description + task_time = 0 + task_sound = f"./sounds/default.mp3" + self.task_slots[str(num)]=Task(task_id, task_name, task_count, task_description, task_time, task_sound, False) + + def delete_task(self, num): + del self.task_slots[str(num)] + + def start_unic_task(self, num): + task_keys = [key for key in list(self.task_slots.keys()) if not self.task_slots[key].finished] + if num in task_keys: + result = num + d = dice() + if d == 5: + self.current_task = self.task_brake + self.current_task_time = timers[d] + self.current_task_time_max = time.time() + self.current_task_time + else: + self.current_task = self.task_slots[result] + self.current_task_time = timers[d] + self.current_task_time_max = time.time() + self.current_task_time + else: + return + + def start_task(self): + task_keys = [key for key in list(self.task_slots.keys()) if not self.task_slots[key].finished] + ic(task_keys) + result = task_keys[randint(0, len(task_keys) - 1)] + d = dice() + if d == 5: + self.current_task = self.task_brake + self.current_task_time = timers[d] + self.current_task_time_max = time.time() + self.current_task_time + else: + self.current_task = self.task_slots[result] + self.current_task_time = timers[d] + self.current_task_time_max = time.time() + self.current_task_time + +def dice(): + return randint(0,5) + +@dataclass +class Task: + id: int + name: str + count: int #1-6 + description: str + time: int #seconds + sound: str #path to sound file + finished: bool + +timers = [ + 600, #10minutes + 1200, #20minutes + 1800, #30minutes + 2400, #40minutes + 3000, #50minutes + 600, #10minutes break +] \ No newline at end of file diff --git a/view.py b/view.py new file mode 100644 index 0000000..6b9131d --- /dev/null +++ b/view.py @@ -0,0 +1,218 @@ +import tkinter as tk +from tktooltip import ToolTip +from tkinter import ttk +from tkinter.messagebox import askyesno +from tkinter.filedialog import askopenfilename, asksaveasfilename +from icecream import ic +from functools import partial +from threading import Thread +import time +import model +import json +import matplotlib.pyplot as plt + + +queue = model.Queue() + +btns = {} + +root = tk.Tk() +root.title("Прям очень Таскер") +root.geometry("250x350") +root.option_add("*tearOff", tk.FALSE) +root.iconbitmap(default=model.resource_path("icon.ico")) + +def save_session(): + path = asksaveasfilename(filetypes=[("Json", '*.json')], defaultextension=".json") + ic(path) + data = { + "tasks": [task.__dict__ for id, task in queue.task_slots.items()], + "break": queue.task_brake.__dict__ + } + with open(path, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=4) + +def build_plt(): + labels = [task.name for id, task in queue.task_slots.items()] + [queue.task_brake.name] + times = [int(task.time/60) for id, task in queue.task_slots.items()] + [int(queue.task_brake.time/60)] + ic(labels) + ic(times) + plt.figure(figsize=(len(labels), 2)) + plt.bar(labels, times) + plt.title("Статистика выполнения") + plt.show() + # plt.savefig("chart.png") + +def load_session(): + path = askopenfilename(filetypes=[("Json", '*.json')]) + ic(path) + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + queue.task_slots = {str(task["id"]): model.Task(**task) for task in data["tasks"]} + queue.task_brake = model.Task(**data["break"]) if data["break"] else None + refrash() + +def refrash(): + for id, task in queue.task_slots.items(): + btns[str(id)]["text"] = f"{id} {task.name} ({task.count})" + if hasattr(btns[str(id)], 'toolTip'): + btns[str(id)].toolTip.on_leave() + btns[str(id)].toolTip.destroy() + btns[str(id)].toolTip = ToolTip(btns[str(id)], msg=task.description) + +def start_task_unic(num, event): + if not queue.run_task: + queue.run_task = True + queue.start_unic_task(str(num)) + btnLabel.bind("", pause_task) + btnLabel.bind("", stop_task) + if hasattr(btnLabel, 'toolTip'): + btnLabel.toolTip.on_leave() + btnLabel.toolTip.destroy() + btnLabel.toolTip = ToolTip(btnLabel, msg=queue.current_task.description) + Thread(target=do_task).start() + +def start_task(event): + if not queue.run_task: + queue.run_task = True + queue.start_task() + btnLabel.bind("", pause_task) + btnLabel.bind("", stop_task) + if hasattr(btnLabel, 'toolTip'): + btnLabel.toolTip.on_leave() + btnLabel.toolTip.destroy() + btnLabel.toolTip = ToolTip(btnLabel, msg=queue.current_task.description) + Thread(target=do_task).start() + +def restore_task(event): + queue.run_task = True + queue.current_task_time_max = queue.current_task_time_current + time.time() + Thread(target=do_task).start() + btnLabel.bind("", pause_task) + +def pause_task(event): + myText = f"{btnLabel.cget('text')}\nПауза" + queue.run_task = False + time.sleep(0.2) + ic(myText) + btnLabel["text"] = myText + btnLabel.bind("", restore_task) + +def stop_task(event): + queue.run_task = False + btnLabel["text"] = "Начать" + btnLabel.bind("", start_task) + if hasattr(btnLabel, 'toolTip'): + btnLabel.toolTip.on_leave() + btnLabel.toolTip.destroy() + btnLabel.toolTip = ToolTip(btnLabel, msg="Начать задачу") + + +def do_task(): + ic(queue.current_task.name) + while queue.current_task_time_max > time.time() and queue.run_task: + queue.current_task_time_current = queue.current_task_time_max - time.time() + m, s = divmod(queue.current_task_time_current, 60) + btnLabel["text"] = f"{queue.current_task.name}\n{int(m)} минут {int(s)} секкунд" + time.sleep(1) + if queue.run_task: + queue.current_task.count += 1 + queue.current_task.time += queue.current_task_time + if queue.current_task.id > 0: + ic(queue.task_slots) + if queue.task_slots[str(queue.current_task.id)].finished: + btns[str(queue.current_task.id)]["text"] = f"{queue.current_task.id} {queue.current_task.name} ({queue.current_task.count})\nЗавершено" + else: + btns[str(queue.current_task.id)]["text"] = f"{queue.current_task.id} {queue.current_task.name} ({queue.current_task.count})" + queue.sound() + stop_task(0) + +def submit(window, btn, num, name, description, event=0): + name = name.get() + description = description.get() + ic(name, description) + btn["text"] = f"{num} {name} (0)" + if hasattr(btn, 'toolTip'): + btn.toolTip.on_leave() + btn.toolTip.destroy() + btn.toolTip = ToolTip(btn, msg=description) + queue.create_task(name, description, num) + window.destroy() + +def finish_unfinished_task(btn, num, event): + if str(num) in queue.task_slots.keys(): + if queue.task_slots[str(num)].finished: + result = askyesno(title="Подтвержение операции", message="Отменить завершение задачи?") + else: + result = askyesno(title="Подтвержение операции", message="Завершить задачу?") + if result: + queue.task_slots[str(num)].finished = False if queue.task_slots[str(num)].finished else True + if queue.task_slots[str(num)].finished: + btns[str(num)]["text"] = f"{queue.task_slots[str(num)].id} {queue.task_slots[str(num)].name} ({queue.task_slots[str(num)].count})\nЗавершено" + else: + btns[str(num)]["text"] = f"{queue.task_slots[str(num)].id} {queue.task_slots[str(num)].name} ({queue.task_slots[str(num)].count})" + + +def click(btn, num, event): + if (str(num) in queue.task_slots.keys()): + start_task_unic(num, 1) + else: + ic(event, btn, num) + window = tk.Toplevel() + window.title("Новая задача") + window.geometry("250x200") + label = ttk.Label(window, text=f"Имя задачи:") + input_name = ttk.Entry(window) + label_description = ttk.Label(window, text=f"Описание задачи (если необходимо):") + input_description = ttk.Entry(window) + window.bind('', partial(submit, window, btn, num, input_name, input_description)) + button_sambit = ttk.Button(window, text="Создать задачу", command = partial(submit, window, btn, num, input_name, input_description)) + label.pack(fill="both") + input_name.pack(expand=True, fill="both") + label_description.pack(fill="both") + input_description.pack(expand=True, fill="both") + input_name.focus() + button_sambit.pack(expand=True, fill="both") + window.grab_set() + window.attributes("-topmost",True) + +def clear_task(btn, num, event): + result = askyesno(title="Подтвержение операции", message="Хотите удалить задачу?") + if result: + btn["text"] = f"{num} ____________ ()" + queue.delete_task(num) + + +for c in range(2): root.columnconfigure(index=c, weight=1) +for r in range(4): root.rowconfigure(index=r, weight=1) + +for i in range(0,6): + btn = ttk.Button(text=f"{i+1} ____________ ()") + btn.bind("", partial(click, btn, i+1)) + btn.bind("", partial(clear_task, btn, i+1)) + btn.bind("", partial(finish_unfinished_task, btn, i+1)) + btn.grid(column=(i%2), row=(i//2), sticky="nsew") + btns[str(i+1)] = btn +ic(btns) + +btnLabel = ttk.Button(text=f"Начать") +btnLabel.bind("", start_task) +btnLabel.bind("", stop_task) +btnLabel.grid(column=0, row=3, columnspan=2, sticky="nsew") + +main_menu = tk.Menu() +file_menu = tk.Menu() +file_menu.add_command(label="Сохранить сессию", command=save_session) +file_menu.add_command(label="Загрузить сессию", command=load_session) +# file_menu.add_separator() +# file_menu.add_command(label="Выход") + +main_menu.add_cascade(label="Файл", menu=file_menu) +main_menu.add_cascade(label="График сессии", command=build_plt) +main_menu.add_cascade(label="Закрепить", command= lambda: root.attributes("-topmost",True)) + +root.config(menu=main_menu) + +root.update_idletasks() +root.mainloop() +