diff --git a/data.py b/data.py index 503bcf5..ab3d3da 100644 --- a/data.py +++ b/data.py @@ -1,10 +1,11 @@ -import os -import json -import base64 import atexit -from datetime import datetime, date, time, timedelta -from threading import Thread, Event +import base64 +import json +import os from collections.abc import MutableMapping +from datetime import datetime, date, timedelta +from threading import Thread, Event +from typing import List from PySide2 import QtCore @@ -15,33 +16,67 @@ tasks_path = os.path.join(data_dir_path, "tasks.json") data_path = os.path.join(data_dir_path, "data_{}.json") save_delay = 3 * 60 +max_jira_tasks = 50 class Tasks: - def __init__(self): - if not os.path.exists(data_dir_path): - os.mkdir(data_dir_path) - if os.path.exists(tasks_path): - with open(tasks_path, "r") as f: - encoded_tasks = json.loads(f.read()) - self._tasks = list(map(lambda x: base64.b64decode(x.encode("utf-8")).decode("utf-8"), encoded_tasks)) + def __init__(self, data): + self._data = data + if "tasks" in self._data: + self._tasks = list(map(lambda x: base64.b64decode(x.encode("utf-8")).decode("utf-8"), self._data["tasks"])) else: self._tasks = [] + if "jira_tasks" in self._data: + self._jira_tasks_usage = dict() + for k, v in self._data["jira_tasks"].items(): + key = base64.b64decode(k.encode("utf-8")).decode("utf-8") + self._jira_tasks_usage[key] = datetime.fromisoformat(v) + self._jira_tasks = sorted(self._jira_tasks_usage.keys(), key=lambda x: self._jira_tasks_usage[x]) + else: + self._jira_tasks_usage = dict() + self._jira_tasks = [] @property - def tasks(self): + def tasks(self) -> List[str]: return self._tasks @tasks.setter def tasks(self, tasks): self._tasks = tasks - self._save() - - def _save(self): - print("... saving tasks ...") encoded_tasks = list(map(lambda x: base64.b64encode(x.encode("utf-8")).decode("utf-8"), self._tasks)) - with open(tasks_path, "w+") as f: - f.write(json.dumps(encoded_tasks)) + self._data["tasks"] = encoded_tasks + + @property + def jira_tasks(self): + return self._jira_tasks + + def add_jira_task(self, task_name): + print(f"before: {self._jira_tasks}") + self._jira_tasks.append(task_name) + print(f"after: {self._jira_tasks}") + self._jira_tasks_usage[task_name] = datetime.now() + if len(self._jira_tasks_usage) > max_jira_tasks: + sorted_tasks = sorted(self._jira_tasks_usage.keys(), key=lambda x: self._jira_tasks_usage[x]) + overhang_tasks = sorted_tasks[:len(sorted_tasks) - max_jira_tasks] + for task in overhang_tasks: + del self._jira_tasks_usage[task] + self._save_jira_tasks() + + def update_jira_task_usage(self, task_name): + if task_name in self._jira_tasks_usage: + self._jira_tasks_usage[task_name] = datetime.now() + self._save_jira_tasks() + + def _save_jira_tasks(self): + serialized = dict() + for k, v in self._jira_tasks_usage.items(): + key = base64.b64encode(k.encode("utf-8")).decode("utf-8") + serialized[key] = datetime.isoformat(v) + self._data["jira_tasks"] = serialized + + @property + def all_tasks(self): + return self.tasks + self.jira_tasks class Data(MutableMapping): @@ -110,8 +145,8 @@ class Data(MutableMapping): class Log: - def __init__(self): - self._data = Data() + def __init__(self, data): + self._data = data def cleanup(): self.log("End") diff --git a/main.py b/main.py index 68e3575..685e6dd 100755 --- a/main.py +++ b/main.py @@ -7,7 +7,7 @@ from functools import partial from PySide2 import QtCore, QtGui, QtWidgets from PySide2.QtWidgets import QMessageBox -from data import Tasks, Log +from data import Tasks, Log, Data from exceptions import FimeFrackingException from new_task import NewTask from task_edit import TaskEdit @@ -18,8 +18,9 @@ class App: def __init__(self): self.app = QtWidgets.QApplication(sys.argv) - self.tasks = Tasks() - self.log = Log() + data = Data() + self.tasks = Tasks(data) + self.log = Log(data) self.active_task = self.log.last_log() or "Nothing" icon = QtGui.QIcon.fromTheme("appointment-new") @@ -27,12 +28,12 @@ class App: self.menu = QtWidgets.QMenu() self.new_task = NewTask(None) - self.new_task.accepted.connect(self.new_task_selected) + self.new_task.accepted.connect(self.new_task_imported) self.taskEdit = TaskEdit(None) self.taskEdit.accepted.connect(self.tasks_edited) - self.reportDialog = Report(None) + self.reportDialog = Report(self.tasks, None) self.reportDialog.accepted.connect(self.report_done) self.tray = QtWidgets.QSystemTrayIcon() @@ -43,8 +44,10 @@ class App: self.update_tray_menu() @QtCore.Slot() - def new_task_selected(self): - print(f"dialog input: {self.new_task.task_text}") + def new_task_imported(self): + if self.new_task.task_text: + self.tasks.add_jira_task(self.new_task.task_text) + self.update_tray_menu() @QtCore.Slot() def tasks_edited(self): @@ -64,21 +67,28 @@ class App: self.update_tray_menu() def update_tray_menu(self): - self.menu.clear() - tasks = list(self.tasks.tasks) - tasks.append("Pause") - if self.active_task == "Nothing": - tasks.append("Nothing") + def add_tasks(tasks): + for t in tasks: + a = self.menu.addAction(t) + a.triggered.connect(partial(self.change_task, t)) + if t == self.active_task: + a.setIcon(QtGui.QIcon.fromTheme("go-next")) - for t in tasks: - a = self.menu.addAction(t) - a.triggered.connect(partial(self.change_task, t)) - if t == self.active_task: - a.setIcon(QtGui.QIcon.fromTheme("go-next")) + self.menu.clear() + + add_tasks(self.tasks.tasks) + + self.menu.addSeparator() + add_tasks(self.tasks.jira_tasks) + + self.menu.addSeparator() + add_tasks(["Pause"]) + if self.active_task == "Nothing": + add_tasks(["Nothing"]) self.menu.addSeparator() - new_action = self.menu.addAction("New task") + new_action = self.menu.addAction("Import Jira task") new_action.triggered.connect(self.new_task_slot) edit_action = self.menu.addAction("Edit tasks") diff --git a/new_task.py b/new_task.py index 8e9973e..81d9e2e 100644 --- a/new_task.py +++ b/new_task.py @@ -15,7 +15,7 @@ class NewTask(QtWidgets.QDialog): ok_button = QtWidgets.QPushButton() ok_button.setText("OK") - ok_button.setIcon(QtGui.QIcon.fromTheme("dialog-ok-apply")) + ok_button.setIcon(QtGui.QIcon.fromTheme("dialog-ok")) ok_button.pressed.connect(self.accept) ok_button.setAutoDefault(True) diff --git a/report.py b/report.py index 7ef1f93..cfe3b5f 100644 --- a/report.py +++ b/report.py @@ -2,10 +2,25 @@ from PySide2 import QtCore, QtGui, QtWidgets from datetime import datetime +from data import Tasks + class Report(QtWidgets.QDialog): - def __init__(self, parent, *args, **kwargs): + class TaskItemCompleter(QtWidgets.QStyledItemDelegate): + def __init__(self, tasks: Tasks, parent=None): + super().__init__(parent) + self._tasks = tasks + + def createEditor(self, parent, *_): + editor = QtWidgets.QLineEdit(parent) + completer = QtWidgets.QCompleter(self._tasks.all_tasks, parent) + completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive) + editor.setCompleter(completer) + return editor + + def __init__(self, tasks: Tasks, parent, *args, **kwargs): super().__init__(parent, *args, **kwargs) + self._tasks = tasks self._report = None self._report_data = None self._changing_items = False @@ -20,6 +35,7 @@ class Report(QtWidgets.QDialog): self.tableWidget.setColumnCount(3) self.tableWidget.setHorizontalHeaderLabels(["Task", "Start time", "Duration"]) self.tableWidget.cellChanged.connect(self.cell_changed) + self.tableWidget.setItemDelegateForColumn(0, Report.TaskItemCompleter(self._tasks, self)) self.header = QtWidgets.QHeaderView(QtCore.Qt.Orientation.Horizontal) self.tableWidget.setHorizontalHeader(self.header) @@ -49,7 +65,7 @@ class Report(QtWidgets.QDialog): ok_button = QtWidgets.QPushButton() ok_button.setText("OK") - ok_button.setIcon(QtGui.QIcon.fromTheme("dialog-ok-apply")) + ok_button.setIcon(QtGui.QIcon.fromTheme("dialog-ok")) ok_button.pressed.connect(self._accept) ok_button.setAutoDefault(True) diff --git a/task_edit.py b/task_edit.py index 56fb5a7..1113a46 100644 --- a/task_edit.py +++ b/task_edit.py @@ -26,7 +26,7 @@ class TaskEdit(QtWidgets.QDialog): ok_button = QtWidgets.QPushButton() ok_button.setText("OK") - ok_button.setIcon(QtGui.QIcon.fromTheme("dialog-ok-apply")) + ok_button.setIcon(QtGui.QIcon.fromTheme("dialog-ok")) ok_button.pressed.connect(self.accept) ok_button.setAutoDefault(True)