A tasks/app.spec => tasks/app.spec +45 -0
@@ 0,0 1,45 @@
+# -*- mode: python ; coding: utf-8 -*-
+
+
+block_cipher = None
+
+
+a = Analysis(
+ ['main.py'],
+ pathex=[],
+ binaries=[],
+ datas=[('resources', 'resources/')],
+ hiddenimports=[],
+ hookspath=[],
+ hooksconfig={},
+ runtime_hooks=[],
+ excludes=[],
+ win_no_prefer_redirects=False,
+ win_private_assemblies=False,
+ cipher=block_cipher,
+ noarchive=False,
+)
+pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
+
+exe = EXE(
+ pyz,
+ a.scripts,
+ a.binaries,
+ a.zipfiles,
+ a.datas,
+ [],
+ name='Tasks',
+ debug=False,
+ bootloader_ignore_signals=False,
+ strip=False,
+ upx=True,
+ upx_exclude=[],
+ runtime_tmpdir=None,
+ console=False,
+ disable_windowed_traceback=False,
+ argv_emulation=False,
+ target_arch=None,
+ codesign_identity=None,
+ entitlements_file=None,
+ icon=['resources/icon.ico'],
+)
A tasks/main.py => tasks/main.py +14 -0
@@ 0,0 1,14 @@
+from pathlib import Path
+from PySide6.QtWidgets import QApplication
+
+from package.main_window import MainWindow
+
+SOURCE_FILE = Path(__file__).resolve()
+SOURCE_DIR = SOURCE_FILE.parent
+DATA_DIR = SOURCE_DIR / "resources"
+
+if __name__ == "__main__":
+ app = QApplication()
+ win = MainWindow(DATA_DIR)
+ win.show()
+ app.exec()
A tasks/package/api/task.py => tasks/package/api/task.py +63 -0
@@ 0,0 1,63 @@
+from pathlib import Path
+import json
+import logging
+
+logging.basicConfig(level=logging.DEBUG)
+
+TASKS_DIR = Path.home() / ".tasks"
+TASKS_FILE = TASKS_DIR / "tasks.json"
+
+
+def get_tasks():
+ if TASKS_FILE.exists():
+ with open(TASKS_FILE, "r") as f:
+ return json.load(f)
+ return {}
+
+
+def add_task(name):
+ tasks = get_tasks()
+ if name in tasks.keys():
+ logging.error("Une tâche avec le même nom existe déjà")
+ return False
+
+ tasks[name] = False
+ _write_tasks(tasks)
+ return True
+
+
+def del_task(name):
+ tasks = get_tasks()
+ if name not in tasks.keys():
+ logging.error("La tâche n'existe pas")
+ return False
+
+ del tasks[name]
+ _write_tasks(tasks)
+ return True
+
+
+def set_task_status(name, done=True):
+ tasks = get_tasks()
+ if name not in tasks.keys():
+ logging.error("La tâche n'existe pas")
+ return False
+
+ tasks[name] = done
+ _write_tasks(tasks)
+ return True
+
+
+def _write_tasks(tasks):
+ if not TASKS_DIR.exists():
+ TASKS_DIR.mkdir()
+
+ with open(TASKS_FILE, "w") as f:
+ json.dump(tasks, f, indent=4)
+ logging.info("Les tâches ont bien été mises à jour")
+
+
+if __name__ == "__main__":
+ add_task("Task 1")
+ set_task_status("Task 1")
+ del_task("Task 1")
A tasks/package/main_window.py => tasks/package/main_window.py +144 -0
@@ 0,0 1,144 @@
+from pathlib import Path
+from PySide6 import QtWidgets, QtGui, QtCore
+
+import package.api.task
+
+
+COLORS = {False: (235, 64, 52), True: (160, 237, 83)}
+
+
+class TaskItem(QtWidgets.QListWidgetItem):
+ def __init__(self, name, done, list_widget):
+ super().__init__(name)
+
+ self.listWidget = list_widget
+ self.name = name
+ self.done = done
+
+ self.listWidget.addItem(self)
+ self.set_background_color()
+
+ def toggle_state(self):
+ self.done = not self.done
+ package.api.task.set_task_status(self.name, self.done)
+ self.set_background_color()
+
+ def set_background_color(self):
+ color = COLORS.get(self.done)
+ self.setBackground(QtGui.QColor(*color))
+ color_str = ", ".join(map(str, color))
+ stylesheet = f"""
+ QListView::item:selected {{
+ background: rgb({color_str});
+ color: rgb(0, 0, 0);
+ }}"""
+ self.listWidget.setStyleSheet(stylesheet)
+
+
+class MainWindow(QtWidgets.QWidget):
+ def __init__(self, app_dir):
+ super().__init__()
+ self.app_dir = app_dir
+ self.setup_ui()
+ self.get_tasks()
+
+ def setup_ui(self):
+ self.create_widgets()
+ self.create_layouts()
+ self.modify_widgets()
+ self.add_widgets_to_layouts()
+ self.setup_connections()
+
+ def create_widgets(self):
+ self.lw_tasks = QtWidgets.QListWidget()
+ self.btn_add = QtWidgets.QPushButton()
+ self.btn_clean = QtWidgets.QPushButton()
+ self.btn_quit = QtWidgets.QPushButton()
+
+ self.tray = QtWidgets.QSystemTrayIcon()
+
+ def modify_widgets(self):
+ self.setWindowTitle("Tasks")
+ self.setWindowIcon(QtGui.QIcon(str(self.app_dir / "icon.ico")))
+ self.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowStaysOnTopHint)
+
+ stylesheet = f"""\
+ QPushButton {{
+ border: none;
+ }}
+ QListView {{
+ border: none;
+ }}
+ QListView::item {{
+ height: 50px;
+ }}"""
+ self.setStyleSheet(stylesheet)
+
+ self.main_layout.setContentsMargins(0, 0, 0, 0)
+ self.main_layout.setSpacing(0)
+
+ self.btn_add.setIcon(QtGui.QIcon(str(self.app_dir / "add.svg")))
+ self.btn_clean.setIcon(QtGui.QIcon(str(self.app_dir / "clean.svg")))
+ self.btn_quit.setIcon(QtGui.QIcon(str(self.app_dir / "close.svg")))
+
+ self.btn_add.setFixedSize(36, 36)
+ self.btn_clean.setFixedSize(36, 36)
+ self.btn_quit.setFixedSize(36, 36)
+
+ self.lw_tasks.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+ self.lw_tasks.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+ self.tray.setIcon(QtGui.QIcon(str(self.app_dir / "icon.png")))
+ self.tray.setVisible(True)
+
+ def create_layouts(self):
+ self.main_layout = QtWidgets.QVBoxLayout(self)
+ self.layout_buttons = QtWidgets.QHBoxLayout()
+
+ def add_widgets_to_layouts(self):
+ self.main_layout.addWidget(self.lw_tasks)
+ self.main_layout.addLayout(self.layout_buttons)
+
+ self.layout_buttons.addWidget(self.btn_add)
+ self.layout_buttons.addStretch()
+ self.layout_buttons.addWidget(self.btn_clean)
+ self.layout_buttons.addWidget(self.btn_quit)
+
+ def setup_connections(self):
+ self.lw_tasks.itemClicked.connect(lambda lw_item: lw_item.toggle_state())
+ self.btn_add.clicked.connect(self.add_tasks)
+ self.btn_clean.clicked.connect(self.clean_tasks)
+ self.btn_quit.clicked.connect(self.close)
+ self.tray.activated.connect(self.tray_clicked)
+
+ def add_tasks(self):
+ task_name, success = QtWidgets.QInputDialog.getText(self,
+ "Ajouter une tâche",
+ "Nom de la tâche :")
+ if success and task_name:
+ package.api.task.add_task(task_name)
+ self.get_tasks()
+
+ def get_tasks(self):
+ self.lw_tasks.clear()
+ tasks = package.api.task.get_tasks()
+ for task_name, done in tasks.items():
+ TaskItem(name=task_name, done=done, list_widget=self.lw_tasks)
+
+ def clean_tasks(self):
+ for i in range(self.lw_tasks.count()):
+ lw_item = self.lw_tasks.item(i)
+ if lw_item.done:
+ package.api.task.del_task(lw_item.name)
+ self.get_tasks()
+
+ def tray_clicked(self):
+ if self.isHidden():
+ self.showNormal()
+ else:
+ self.hide()
+
+ def center_under_tray(self):
+ tray_x = self.tray.geometry().x()
+ w, h = self.sizeHint().toTuple()
+ self.move(tray_x - (w / 2), 25)
A tasks/resources/add.svg => tasks/resources/add.svg +1 -0
@@ 0,0 1,1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/><path d="M0 0h24v24H0z" fill="none"/></svg><
\ No newline at end of file
A tasks/resources/clean.svg => tasks/resources/clean.svg +1 -0
@@ 0,0 1,1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M15 16h4v2h-4zm0-8h7v2h-7zm0 4h6v2h-6zM3 18c0 1.1.9 2 2 2h6c1.1 0 2-.9 2-2V8H3v10zM14 5h-3l-1-1H6L5 5H2v2h12z"/><path fill="none" d="M0 0h24v24H0z"/></svg><
\ No newline at end of file
A tasks/resources/close.svg => tasks/resources/close.svg +1 -0
@@ 0,0 1,1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0V0z"/><path d="M18.3 5.71c-.39-.39-1.02-.39-1.41 0L12 10.59 7.11 5.7c-.39-.39-1.02-.39-1.41 0-.39.39-.39 1.02 0 1.41L10.59 12 5.7 16.89c-.39.39-.39 1.02 0 1.41.39.39 1.02.39 1.41 0L12 13.41l4.89 4.89c.39.39 1.02.39 1.41 0 .39-.39.39-1.02 0-1.41L13.41 12l4.89-4.89c.38-.38.38-1.02 0-1.4z"/></svg><
\ No newline at end of file
A tasks/resources/icon.ico => tasks/resources/icon.ico +0 -0
A tasks/resources/icon.png => tasks/resources/icon.png +0 -0