diff --git a/src/services/todo.py b/src/services/todo.py index 54e47c9..ff31dbe 100644 --- a/src/services/todo.py +++ b/src/services/todo.py @@ -1,30 +1,42 @@ +# Standard library imports import json -import os +import uuid from datetime import datetime from pathlib import Path -from fabric.core.service import Service -from fabric.utils import logger +# Fabric imports +from fabric.core.service import Property, Service +# Local imports import shared.data as data class TodoService(Service): """Service for managing persistent todo list with JSON storage""" - def __init__(self, **kwargs): - super().__init__(**kwargs) + def __init__(self): + super().__init__() self._todos = [] self._file_path = self._get_todos_file_path() self._load_todos() self._callbacks = [] - def _notify_callbacks(self, event: str, data=None): + def add_callback(self, callback): + """Add a callback function to be notified of changes""" + self._callbacks.append(callback) + + def remove_callback(self, callback): + """Remove a callback function""" + if callback in self._callbacks: + self._callbacks.remove(callback) + + def _notify_callbacks(self, event_type, data=None): + """Notify all registered callbacks of changes""" for callback in self._callbacks: try: - callback(event, data) + callback(event_type, data) except Exception as e: - logger.error(f"Error in todo callback: {e}") + print(f"Error in todo callback: {e}") def _get_todos_file_path(self): """Returns the path to the todos JSON file""" @@ -36,23 +48,23 @@ class TodoService(Service): """Load todos from JSON file""" try: if self._file_path.exists(): - with open(self._file_path, "r") as f: + with open(self._file_path, "r", encoding="utf-8") as f: self._todos = json.load(f) else: self._todos = [] except Exception as e: - logger.error(f"Error loading todos: {e}") + print(f"Error loading todos: {e}") self._todos = [] def _save_todos(self): """Save todos to JSON file""" try: - with open(self._file_path, "w") as f: + with open(self._file_path, "w", encoding="utf-8") as f: json.dump(self._todos, f, indent=2, ensure_ascii=False) except Exception as e: - logger.error(f"Error saving todos: {e}") + print(f"Error saving todos: {e}") - @property + @Property(list, "readable") def todos(self): """Get all todos""" return self._todos.copy() @@ -60,10 +72,10 @@ class TodoService(Service): def add_todo(self, text: str, priority: str = "medium") -> dict: """Add a new todo item""" todo = { - "id": os.urandom(8).hex(), + "id": str(uuid.uuid4()), "text": text, "completed": False, - "priority": priority, + "priority": priority, # low, medium, high "created_at": datetime.now().isoformat(), "updated_at": datetime.now().isoformat(), } @@ -110,11 +122,7 @@ class TodoService(Service): def set_priority(self, todo_id: str, priority: str) -> bool: """Set the priority of a todo item""" - valid_priorities = ["low", "medium", "high"] - if priority not in valid_priorities: - logger.warning( - f"Invalid priority '{priority}'. Must be one of {valid_priorities}" - ) + if priority not in ["low", "medium", "high"]: return False for todo in self._todos: @@ -127,6 +135,13 @@ class TodoService(Service): return True return False + def get_todo(self, todo_id: str) -> dict | None: + """Get a specific todo by ID""" + for todo in self._todos: + if todo["id"] == todo_id: + return todo.copy() + return None + def clear_completed(self): """Remove all completed todos""" initial_count = len(self._todos) @@ -139,28 +154,20 @@ class TodoService(Service): """Get todo statistics""" total = len(self._todos) completed = sum(1 for todo in self._todos if todo["completed"]) + pending = total - completed + return { "total": total, "completed": completed, - "active": total - completed, + "pending": pending, + "completion_rate": (completed / total * 100) if total > 0 else 0, } - def add_callback(self, callback): - """Add a callback for todo events""" - if callback not in self._callbacks: - self._callbacks.append(callback) - def remove_callback(self, callback): - """Remove a callback""" - if callback in self._callbacks: - self._callbacks.remove(callback) - - -_todo_service = None +# Global service instance +todo_service = TodoService() def get_todo_service() -> TodoService: - global _todo_service - if _todo_service is None: - _todo_service = TodoService() - return _todo_service + """Get the global todo service instance""" + return todo_service diff --git a/src/shared/window/applet_window.py b/src/shared/window/applet_window.py index 186ff10..0e9b5e0 100644 --- a/src/shared/window/applet_window.py +++ b/src/shared/window/applet_window.py @@ -24,12 +24,12 @@ class DismissLayer(Window): class AppletWindow(PopupWindow): _active_popup = None - def __init__(self, **kwargs): + def __init__(self, edge_margin: int = 0, **kwargs): self._is_open = False self._hide_timeout_id = None self.dismiss_layer = DismissLayer(on_dismiss=self.toggle) kwargs["keyboard_mode"] = "on-demand" - super().__init__(**kwargs) + super().__init__(edge_margin=edge_margin, **kwargs) self.add_keybinding("escape", lambda *_: self.toggle()) def toggle(self, *_): diff --git a/src/shared/window/popup_win.py b/src/shared/window/popup_win.py index 3484764..d9eb957 100644 --- a/src/shared/window/popup_win.py +++ b/src/shared/window/popup_win.py @@ -2,8 +2,6 @@ from fabric.utils import Gdk, GLib, Gtk, logger from fabric.widgets.wayland import WaylandWindow from gi.repository import GtkLayerShell -EDGE_MARGIN = 0 - def _get_monitor_geometry(widget: Gtk.Widget) -> tuple[int, int]: screen = Gdk.Screen.get_default() @@ -24,6 +22,7 @@ class PopupWindow(WaylandWindow): parent: WaylandWindow | None = None, pointing_to: Gtk.Widget | None = None, margin: tuple[int, ...] | str = "0px 0px 0px 0px", + edge_margin: int = 0, **kwargs, ): super().__init__(**kwargs) @@ -33,6 +32,7 @@ class PopupWindow(WaylandWindow): self._pointing_widget = pointing_to self._base_margin = self.extract_margin(margin) self.margin = self._base_margin.values() + self.edge_margin = edge_margin self.connect("notify::visible", self.do_update_handlers) def get_coords_for_widget(self, widget: Gtk.Widget) -> tuple[int, int]: @@ -163,15 +163,15 @@ class PopupWindow(WaylandWindow): raw_margin = round((bar_left + coords_centered[0]) - (width / 2)) else: raw_margin = round((parent_x_margin + coords_centered[0]) - (width / 2)) - min_margin = EDGE_MARGIN - max_margin = monitor_width - width - EDGE_MARGIN + min_margin = self.edge_margin + max_margin = monitor_width - width - self.edge_margin clamped = max(min_margin, min(raw_margin, max_margin)) position_margins = (0, 0, 0, clamped) else: raw_margin = round((parent_y_margin + coords_centered[1]) - (height / 2)) monitor_height = Gdk.Screen.get_default().get_height() - min_margin = EDGE_MARGIN - max_margin = monitor_height - height - EDGE_MARGIN + min_margin = self.edge_margin + max_margin = monitor_height - height - self.edge_margin clamped = max(min_margin, min(raw_margin, max_margin)) position_margins = (clamped, 0, 0, 0) diff --git a/src/styles/main.css b/src/styles/main.css index 37f362a..b0daba6 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -15,3 +15,5 @@ font-family: "SF Pro Rounded"; } +@import url("widgets/pomodoro.css"); +@import url("widgets/todo.css"); diff --git a/src/styles/widgets/pomodoro.css b/src/styles/widgets/pomodoro.css new file mode 100644 index 0000000..7f40653 --- /dev/null +++ b/src/styles/widgets/pomodoro.css @@ -0,0 +1,122 @@ + +#pomodoro-popup { + background-color: @bg-a50; + border-radius: apply(radius-md); + box-shadow: inset 0 0 0 1px alpha(@text-dim, 0.4); + padding: 15px; + margin-right: 20px; + border: 1px solid @border-strong; +} + +#pomodoro-container { + padding: 10px; + background-color: transparent; +} + +#pomodoro-time { + font-size: 38px; + font-weight: 800; + color: @foreground; + font-family: "SF Pro Rounded"; +} + +#pomodoro-cycle { + font-size: 13px; + font-weight: 500; + color: @fg-a60; + font-family: "SF Pro Rounded"; +} + +#pomodoro-title { + font-size: 14px; + font-weight: 700; + color: @foreground; + font-family: "SF Pro Rounded"; +} + +/* Base progress bar color */ +#pomodoro-progress { + background-color: transparent; + color: alpha(@primary, 0.3); + border-color: @primary; +} + +/* Short break color */ +#pomodoro-progress-short { + background-color: transparent; + color: alpha(@success, 0.3); + border-color: @success; +} + +/* Long break color */ +#pomodoro-progress-long { + background-color: transparent; + color: alpha(@error, 0.3); + border-color: @error; +} + +#pomodoro-phase-btn { + background-color: @fg-a18; + color: alpha(@theme_fg_color, 0.7); + border-radius: apply(radius-sm); + padding: 5px 14px; + font-weight: 600; + font-size: 12px; +} + +#pomodoro-phase-btn:hover { + background-color: @fg-a25; + color: @theme_fg_color; +} + +#pomodoro-phase-btn.active { + background-color: @primary; + color: white; +} + +#pomodoro-start-btn { + background-color: @primary; + color: white; + border-radius: apply(radius-sm); + padding: 6px 20px; + font-weight: bold; + font-size: 14px; +} + +#pomodoro-start-btn:hover { + background-color: alpha(@primary, 0.8); +} + +#pomodoro-reset-btn, #pomodoro-secondary-btn { + background-color: @fg-a18; + color: @theme_fg_color; + border-radius: apply(radius-sm); + padding: 6px 15px; + font-weight: 600; + font-size: 14px; +} + +#pomodoro-reset-btn:hover, #pomodoro-secondary-btn:hover { + background-color: @fg-a25; +} + +#pomodoro-spin-box { + background-color: @fg-a18; + border-radius: apply(radius-sm); + padding: 2px; +} + +#pomodoro-spin-btn { + background: transparent; + border: none; + box-shadow: none; + padding: 2px 4px; + min-height: 16px; + color: @theme_fg_color; + border-radius: apply(radius-sm); + font-size: 10px; +} + +#pomodoro-spin-btn:hover { + background-color: @fg-a25; +} diff --git a/src/styles/widgets/todo.css b/src/styles/widgets/todo.css new file mode 100644 index 0000000..123caea --- /dev/null +++ b/src/styles/widgets/todo.css @@ -0,0 +1,265 @@ + +/* === Main Todo List === */ +#todo-list-menu { + background-color: @bg-a50; + border-radius: apply(radius-md); + box-shadow: inset 0 0 0 1px alpha(@text-dim, 0.4); + padding: 15px; + margin-right: 20px; + border: 1px solid @border-strong; +} + +#todo-main-container { + padding: 5px; + background-color: transparent; +} + +#todo-header { + padding: 0; + margin-bottom: 5px; +} + +#todo-title { + font-size: 16px; + font-weight: 700; + color: @foreground; + font-family: "SF Pro Rounded"; +} + +#todo-stats { + font-size: 12px; + color: @fg-a60; + font-family: "SF Pro Rounded"; +} + +/* === Add Task Section === */ +#todo-add-section { + background-color: @fg-a18; + border-radius: apply(radius-sm); + padding: 8px 10px; +} + +#new-todo-entry { + background: transparent; + border: none; + color: @foreground; + font-size: 13px; + font-family: "SF Pro Rounded"; + min-height: 24px; +} + +#new-todo-entry placeholder { + color: @fg-a40; +} + +#add-todo-button { + background-color: @primary; + border-radius: apply(radius-full); + padding: 6px; + min-width: 24px; + min-height: 24px; +} + +#add-todo-button:hover { + background-color: alpha(@primary, 0.8); +} + +/* === Todo Items === */ +#todos-container { + padding: 0; +} + +#todo-item { + background-color: transparent; + border-radius: apply(radius-sm); + padding: 8px 6px; + transition: background-color 0.2s ease; +} + +#todo-item:hover { + background-color: alpha(@fg-a10, 0.3); +} + +#todo-divider { + min-height: 1px; + background-color: alpha(@fg-a20, 0.5); + margin: 0 4px; +} + +/* === Progress Ring === */ +#todo-progress-ring { + background-color: transparent; + color: alpha(@foreground, 0.15); + border-color: @foreground; +} + +/* === Checkbox === */ +#todo-checkbox { + background: transparent; + border: none; + box-shadow: none; + padding: 0; + min-width: 18px; + min-height: 18px; +} + +#todo-checkbox:hover { + background-color: alpha(@fg-a18, 0.5); + border-radius: apply(radius-sm); +} + +/* === Task Text === */ +#todo-text { + font-size: 14px; + font-weight: 500; + color: @foreground; + font-family: "SF Pro Rounded"; +} + +/* === Options Button === */ +#todo-options-btn { + background: transparent; + border: none; + box-shadow: none; + padding: 4px 8px; + min-width: 24px; + min-height: 24px; + font-size: 16px; + color: @fg-a40; + opacity: 0; + transition: opacity 0.2s ease; +} + +#todo-item:hover #todo-options-btn { + opacity: 1; +} + +#todo-options-btn:hover { + color: @foreground; + background-color: alpha(@fg-a18, 0.5); + border-radius: apply(radius-sm); +} + +/* === Clear + Footer === */ +#clear-completed-button { + background: transparent; + border: none; + font-size: 12px; + color: @fg-a40; + padding: 4px; +} + +#clear-completed-button:hover { + color: @foreground; +} + +#todo-footer { + font-size: 11px; + color: @fg-a40; + font-family: "SF Pro Rounded"; + margin-top: 4px; +} + +/* === Options Popover === */ +#todo-options-popover { + background-color: @bg-a50; + border-radius: apply(radius-md); + box-shadow: inset 0 0 0 1px alpha(@text-dim, 0.4); + padding: 10px; + margin-right: 20px; + border: 1px solid @border-strong; + min-width: 340px; +} + +#todo-options-container { + background-color: transparent; +} + +#todo-opt-title { + font-size: 14px; + font-weight: 700; + color: @foreground; + font-family: "SF Pro Rounded"; +} + +#todo-opt-subtitle { + font-size: 11px; + color: @fg-a60; + font-family: "SF Pro Rounded"; +} + +/* === Color Swatches === */ +#todo-color-swatch { + background-color: @fg-a18; + border-radius: apply(radius-full); + padding: 0; + min-width: 28px; + min-height: 28px; + border: 2px solid transparent; + transition: border-color 0.15s ease; +} + +#todo-color-swatch:hover { + border-color: alpha(@foreground, 0.3); +} + +#todo-color-swatch.selected { + border-color: @foreground; + box-shadow: 0 0 0 2px alpha(@foreground, 0.2); +} + +/* === Deadline Pill === */ +#todo-deadline-pill { + background-color: @fg-a18; + border-radius: apply(radius-full); + padding: 6px 12px; +} + +#todo-deadline-value { + font-size: 12px; + color: @foreground; + font-family: "SF Pro Rounded"; + min-width: 70px; +} + +/* === Stepper (reuse pomodoro style) === */ +#todo-spin-box { + background-color: @fg-a10; + border-radius: apply(radius-sm); + padding: 2px; +} + +#todo-spin-btn { + background: transparent; + border: none; + box-shadow: none; + padding: 2px 4px; + min-height: 14px; + color: @theme_fg_color; + border-radius: apply(radius-sm); + font-size: 8px; +} + +#todo-spin-btn:hover { + background-color: @fg-a25; +} + +#todo-spin-value { + font-size: 12px; + color: @foreground; + font-family: "SF Pro Rounded"; +} + +/* === Preset Buttons === */ +#todo-preset-btn { + background-color: @fg-a18; + border-radius: apply(radius-full); + padding: 6px 12px; + font-size: 11px; + color: @foreground; + font-family: "SF Pro Rounded"; +} + +#todo-preset-btn:hover { + background-color: @fg-a25; +}