From 03fb3b23c28b39460eb000dc7d96ada60b1991bc Mon Sep 17 00:00:00 2001 From: Gustavo Henrique Santos Souza de Miranda Date: Sat, 7 Jun 2025 01:01:16 -0300 Subject: [PATCH 01/11] Added Mocks to help aid the process of creation of the textual TUI --- src/pilgrim/application.py | 4 ++ src/pilgrim/service/mocks/__init__.py | 0 .../service/mocks/entry_service_mock.py | 47 +++++++++++++++++++ .../service/mocks/photo_service_mock.py | 42 +++++++++++++++++ .../service/mocks/service_manager_mock.py | 19 ++++++++ .../mocks/travel_diary_service_mock.py | 33 +++++++++++++ src/pilgrim/ui/__init__.py | 0 src/pilgrim/ui/ui.py | 9 ++++ 8 files changed, 154 insertions(+) create mode 100644 src/pilgrim/service/mocks/__init__.py create mode 100644 src/pilgrim/service/mocks/entry_service_mock.py create mode 100644 src/pilgrim/service/mocks/photo_service_mock.py create mode 100644 src/pilgrim/service/mocks/service_manager_mock.py create mode 100644 src/pilgrim/service/mocks/travel_diary_service_mock.py create mode 100644 src/pilgrim/ui/__init__.py create mode 100644 src/pilgrim/ui/ui.py diff --git a/src/pilgrim/application.py b/src/pilgrim/application.py index 61d75ba..8954411 100644 --- a/src/pilgrim/application.py +++ b/src/pilgrim/application.py @@ -1,13 +1,17 @@ from pilgrim.database import Database +from pilgrim.service.mocks.service_manager_mock import ServiceManagerMock from pilgrim.service.servicemanager import ServiceManager +from pilgrim.ui.ui import UIApp class Application: def __init__(self): self.database = Database() + self.ui = UIApp(ServiceManagerMock()) def run(self): self.database.create() + self.ui.run() def get_service_manager(self): session = self.database.session() diff --git a/src/pilgrim/service/mocks/__init__.py b/src/pilgrim/service/mocks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/pilgrim/service/mocks/entry_service_mock.py b/src/pilgrim/service/mocks/entry_service_mock.py new file mode 100644 index 0000000..a091a52 --- /dev/null +++ b/src/pilgrim/service/mocks/entry_service_mock.py @@ -0,0 +1,47 @@ +from typing import List +from pilgrim.service.entry_service import EntryService +from pilgrim.models.entry import Entry + + +class EntryServiceMock(EntryService): + def __init__(self): + super().__init__(None) + + self.mock_data = { + 1: Entry(title="The Adventure Begins", text="I'm hopping in the Plane to finally visit canadian lands", + date="26/07/2025", travel_diary_id=1, id=1, + photos=[]), + 2: Entry(title="The Landing", text="Finally on Canadian Soil", date="27/07/2025", + travel_diary_id=1, id=2,photos=[]), + 3: Entry(title="The Mount Royal", text="The Mount Royal is fucking awesome", date="28/07/2025", + travel_diary_id=1, id=3, photos=[]), + } + self._next_id = 4 + + def create(self, travel_diary_id: int, title: str, text: str, date: str) -> Entry: + new_entry = Entry(title, text, date, travel_diary_id, id=self._next_id) + self.mock_data[self._next_id] = new_entry + self._next_id += 1 + return new_entry + + def read_by_id(self, entry_id: int) -> Entry | None: + return self.mock_data.get(entry_id) + + def read_all(self) -> List[Entry]: + return list(self.mock_data.values()) + + def update(self, entry_id: int, entry_dst: Entry) -> Entry | None: + item_to_update = self.mock_data.get(entry_id) + if item_to_update: + item_to_update.title = entry_dst.title if entry_dst.title is not None else item_to_update.title + item_to_update.text = entry_dst.text if entry_dst.text is not None else item_to_update.text + item_to_update.date = entry_dst.date if entry_dst.date is not None else item_to_update.date + item_to_update.fk_travel_diary_id = entry_dst.fk_travel_diary_id if (entry_dst.fk_travel_diary_id + is not None) else entry_dst.id + item_to_update.photos.extend(entry_dst.photos) + + return item_to_update + return None + + def delete(self, entry_id: int) -> Entry | None: + return self.mock_data.pop(entry_id, None) \ No newline at end of file diff --git a/src/pilgrim/service/mocks/photo_service_mock.py b/src/pilgrim/service/mocks/photo_service_mock.py new file mode 100644 index 0000000..1b02c56 --- /dev/null +++ b/src/pilgrim/service/mocks/photo_service_mock.py @@ -0,0 +1,42 @@ +from pathlib import Path +from typing import List + +from pilgrim.models.photo import Photo +from pilgrim.service.photo_service import PhotoService + + +class PhotoServiceMock(PhotoService): + def __init__(self): + super().__init__(None) + self.mock_data = {} + self._next_id = 1 + + def create(self, filepath: Path, name: str, travel_diary_id, addition_date=None, caption=None) -> Photo | None: + new_photo = Photo(filepath, name, addition_date=addition_date, caption=caption) + self.mock_data[self._next_id] = new_photo + self._next_id += 1 + return new_photo + + + def read_by_id(self, photo_id: int) -> Photo: + return self.mock_data.get(photo_id) + + def read_all(self) -> List[Photo]: + return list(self.mock_data.values()) + + def update(self, photo_id: Photo, photo_dst: Photo) -> Photo | None: + item_to_update:Photo = self.mock_data.get(photo_id) + if item_to_update: + item_to_update.filepath = photo_dst.filepath if photo_dst.filepath else item_to_update.filepath + item_to_update.name = photo_dst.name if photo_dst.name else item_to_update.name + item_to_update.caption = photo_dst.caption if photo_dst.caption else item_to_update.caption + item_to_update.addition_date = photo_dst.addition_date if photo_dst.addition_date\ + else item_to_update.addition_date + item_to_update.fk_travel_diary_id = photo_dst.fk_travel_diary_id if photo_dst.fk_travel_diary_id \ + else item_to_update.fk_travel_diary_id + item_to_update.entries.extend(photo_dst.entries) + return item_to_update + return None + + def delete(self, photo_id: int) -> Photo | None: + return self.mock_data.pop(photo_id, None) diff --git a/src/pilgrim/service/mocks/service_manager_mock.py b/src/pilgrim/service/mocks/service_manager_mock.py new file mode 100644 index 0000000..4741b15 --- /dev/null +++ b/src/pilgrim/service/mocks/service_manager_mock.py @@ -0,0 +1,19 @@ +from pilgrim.service.mocks.entry_service_mock import EntryServiceMock +from pilgrim.service.mocks.photo_service_mock import PhotoServiceMock +from pilgrim.service.mocks.travel_diary_service_mock import TravelDiaryServiceMock +from pilgrim.service.photo_service import PhotoService +from pilgrim.service.servicemanager import ServiceManager + + +class ServiceManagerMock(ServiceManager): + def __init__(self): + super().__init__() + + def get_entry_service(self): + return EntryServiceMock() + + def get_travel_diary_service(self): + return TravelDiaryServiceMock() + + def get_photo_service(self): + return PhotoServiceMock() \ No newline at end of file diff --git a/src/pilgrim/service/mocks/travel_diary_service_mock.py b/src/pilgrim/service/mocks/travel_diary_service_mock.py new file mode 100644 index 0000000..a6b409b --- /dev/null +++ b/src/pilgrim/service/mocks/travel_diary_service_mock.py @@ -0,0 +1,33 @@ +from pilgrim.service.travel_diary_service import TravelDiaryService +from pilgrim.models.travel_diary import TravelDiary + + +class TravelDiaryServiceMock(TravelDiaryService): + def __init__(self): + super().__init__(None) + self.mock_data = { + 1:TravelDiary(id=1,name="Montreal") + } + self._next_id = 2 + + def create(self, name: str): + new_travel_diary = TravelDiary(id=self._next_id,name=name) + self.mock_data[self._next_id] = new_travel_diary + self._next_id += 1 + return new_travel_diary + + def read_by_id(self, travel_id: int): + return self.mock_data[travel_id] + + def read_all(self): + return list(self.mock_data.values()) + + def update(self, travel_diary_id: int, travel_diary_dst: TravelDiary): + item_to_update = self.mock_data.get(travel_diary_id) + if item_to_update: + item_to_update.name = travel_diary_dst.name if travel_diary_dst.name is not None else item_to_update.name + return item_to_update + return None + + def delete(self, travel_diary_id: int): + return self.mock_data.pop(travel_diary_id, None) diff --git a/src/pilgrim/ui/__init__.py b/src/pilgrim/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/pilgrim/ui/ui.py b/src/pilgrim/ui/ui.py new file mode 100644 index 0000000..bf8c7ed --- /dev/null +++ b/src/pilgrim/ui/ui.py @@ -0,0 +1,9 @@ +from textual.app import App + +from pilgrim.service.servicemanager import ServiceManager + + +class UIApp(App): + def __init__(self,service_manager: ServiceManager): + super().__init__() + self.service_manager = service_manager From 58c7bfd8e92b5504ab12de5fedb5e137c41a0bd8 Mon Sep 17 00:00:00 2001 From: Gustavo Henrique Santos Souza de Miranda Date: Sun, 8 Jun 2025 22:50:26 -0300 Subject: [PATCH 02/11] Added the diary select screen --- pyproject.toml | 4 +- requirements.txt | 2 + .../mocks/travel_diary_service_mock.py | 5 +- src/pilgrim/service/travel_diary_service.py | 10 +- src/pilgrim/ui/screens/__init__.py | 0 src/pilgrim/ui/screens/diary_list_screen.py | 147 ++++++++++++++++++ src/pilgrim/ui/styles/pilgrim.css | 25 +++ src/pilgrim/ui/ui.py | 12 ++ 8 files changed, 197 insertions(+), 8 deletions(-) create mode 100644 src/pilgrim/ui/screens/__init__.py create mode 100644 src/pilgrim/ui/screens/diary_list_screen.py create mode 100644 src/pilgrim/ui/styles/pilgrim.css diff --git a/pyproject.toml b/pyproject.toml index 805875b..2ade3e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,9 @@ classifiers = [ "Operating System :: OS Independent", ] dependencies = [ - "sqlalchemy" + "sqlalchemy", + "textual", + "textual-dev" ] [template.plugins.default] src-layout = true diff --git a/requirements.txt b/requirements.txt index f23f26c..5b6b3a2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,5 @@ greenlet==3.2.2 SQLAlchemy==2.0.41 typing_extensions==4.14.0 + +textual~=3.3.0 diff --git a/src/pilgrim/service/mocks/travel_diary_service_mock.py b/src/pilgrim/service/mocks/travel_diary_service_mock.py index a6b409b..625efa8 100644 --- a/src/pilgrim/service/mocks/travel_diary_service_mock.py +++ b/src/pilgrim/service/mocks/travel_diary_service_mock.py @@ -6,9 +6,10 @@ class TravelDiaryServiceMock(TravelDiaryService): def __init__(self): super().__init__(None) self.mock_data = { - 1:TravelDiary(id=1,name="Montreal") + 1:TravelDiary(id=1,name="Montreal"), + 2:TravelDiary(id=2,name="Rio de Janeiro"), } - self._next_id = 2 + self._next_id = 3 def create(self, name: str): new_travel_diary = TravelDiary(id=self._next_id,name=name) diff --git a/src/pilgrim/service/travel_diary_service.py b/src/pilgrim/service/travel_diary_service.py index 0be4cc0..ce5293b 100644 --- a/src/pilgrim/service/travel_diary_service.py +++ b/src/pilgrim/service/travel_diary_service.py @@ -18,18 +18,18 @@ class TravelDiaryService: def read_all(self): return self.session.query(TravelDiary).all() - def update(self, travel_diary_src:TravelDiary,travel_diary_dst:TravelDiary): - original = self.read_by_id(travel_diary_src.id) + def update(self, travel_diary_id: TravelDiary, travel_diary_dst: TravelDiary): + original = self.read_by_id(travel_diary_id.id) if original is not None: original.name = travel_diary_dst.name self.session.commit() self.session.refresh(original) return original - def delete(self, travel_diary_src:TravelDiary): - excluded = self.read_by_id(travel_diary_src.id) + def delete(self, travel_diary_id: TravelDiary): + excluded = self.read_by_id(travel_diary_id.id) if excluded is not None: - self.session.delete(travel_diary_src) + self.session.delete(travel_diary_id) self.session.commit() return excluded return None diff --git a/src/pilgrim/ui/screens/__init__.py b/src/pilgrim/ui/screens/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/pilgrim/ui/screens/diary_list_screen.py b/src/pilgrim/ui/screens/diary_list_screen.py new file mode 100644 index 0000000..1c83f44 --- /dev/null +++ b/src/pilgrim/ui/screens/diary_list_screen.py @@ -0,0 +1,147 @@ +from click import prompt +from textual.app import ComposeResult +from textual.screen import Screen +from textual.widgets import Header, Footer, Label, Static, OptionList, Button +from textual.binding import Binding +from textual.containers import Vertical, Container, Horizontal + + +class DiaryListScreen(Screen): + TITLE = "Pilgrim - Main" + + BINDINGS = [ + Binding("n", "new_diary", "Novo Diário"), + Binding("^q", "quit", "Sair"), + ] + + def __init__(self): + super().__init__() + self.selected_diary_index = None # Armazena o índice do diário selecionado + + def compose(self) -> ComposeResult: + yield Header() + yield Container( + Static("Pilgrim", classes="app-title"), + Label("Select a diary"), + OptionList(id="option-list", classes="diary-options"), + Horizontal( + Button("New diary", id="new-diary"), + + Button("Edit diary", id="edit-diary"), + Button("🔄 Refresh", id="refresh-btn"), + classes="actions-buttons", + ), + classes="dialog-container" + ) + yield Static( + "Tip: use ↑↓ to navigate • ENTER to Select • " + "TAB to alternate the fields • SHIFT + TAB to alternate back ", + classes="tips" + ) + yield Footer() + + def on_mount(self) -> None: + self.refresh_diaries() + self.update_buttons_state() # Atualiza estado inicial dos botões + + def refresh_diaries(self): + try: + service_manager = self.app.service_manager + option_list = self.query_one(".diary-options") + option_list.clear_options() + travel_diary_service = service_manager.get_travel_diary_service() + diaries = travel_diary_service.read_all() + + if not diaries: + # Para OptionList vazio, você pode adicionar uma string simples + option_list.add_option("[dim]Nenhum diário encontrado. Pressione 'N' para criar um novo![/dim]") + else: + for diary in diaries: + # Adiciona cada opção como string com markup rich + option_list.add_option(f"[b]{diary.name}[/b]\n[dim]ID: {diary.id}[/dim]") + + except Exception as e: + self.notify("Error: " + str(e)) + + def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None: + """Handle quando uma opção é selecionada""" + diaries = self.app.service_manager.get_travel_diary_service().read_all() + + if diaries and event.option_index < len(diaries): + self.selected_diary_index = event.option_index + selected_diary = diaries[event.option_index] + self.notify(f"Diário selecionado: {selected_diary.name}") + else: + # Caso seja a opção "nenhum diário encontrado" + self.selected_diary_index = None + + self.update_buttons_state() + + def update_buttons_state(self): + """Atualiza o estado dos botões baseado na seleção""" + edit_button = self.query_one("#edit-diary") + + + # Só habilita os botões se há um diário selecionado + has_selection = self.selected_diary_index is not None + edit_button.disabled = not has_selection + + + def on_button_pressed(self, event: Button.Pressed) -> None: + """Handle cliques nos botões""" + button_id = event.button.id + + if button_id == "new-diary": + self.action_new_diary() + elif button_id == "edit-diary": + self.action_edit_diary() + elif button_id == "refresh-btn": + self.refresh_diaries() + self.notify("Lista atualizada manualmente!") + + def action_new_diary(self): + """Ação para criar novo diário""" + self.notify("Criando novo diário...") + # Aqui você pode navegar para uma tela de criação de diário + + def action_edit_diary(self): + """Ação para editar diário selecionado""" + if self.selected_diary_index is not None: + diaries = self.app.service_manager.get_travel_diary_service().read_all() + if self.selected_diary_index < len(diaries): + selected_diary = diaries[self.selected_diary_index] + self.notify(f"Editando diário: {selected_diary.name}") + # Aqui você pode navegar para uma tela de edição + # self.app.push_screen(EditDiaryScreen(diary=selected_diary)) + + def refresh_diaries(self): + """Atualiza a lista de diários no OptionList""" + try: + service_manager = self.app.service_manager + option_list = self.query_one("#option-list") # Usando ID em vez de classe + + # Debug + current_count = len(option_list.options) if hasattr(option_list, 'options') else 0 + self.notify(f"OptionList atual tem {current_count} opções") + + option_list.clear_options() + + travel_diary_service = service_manager.get_travel_diary_service() + diaries = travel_diary_service.read_all() + + self.notify(f"Carregando {len(diaries)} diários do serviço") + + if not diaries: + option_list.add_option("[dim]Nenhum diário encontrado. Pressione 'N' para criar um novo![/dim]") + self.selected_diary_index = None + else: + for diary in diaries: + option_list.add_option(f"[b]{diary.name}[/b]\n[dim]ID: {diary.id}[/dim]") + + # Valida se a seleção ainda é válida + if (self.selected_diary_index is not None and + self.selected_diary_index >= len(diaries)): + self.selected_diary_index = None + + except Exception as e: + self.notify("Error no refresh_diaries: " + str(e)) \ No newline at end of file diff --git a/src/pilgrim/ui/styles/pilgrim.css b/src/pilgrim/ui/styles/pilgrim.css new file mode 100644 index 0000000..b6f5f6a --- /dev/null +++ b/src/pilgrim/ui/styles/pilgrim.css @@ -0,0 +1,25 @@ + +.dialog-container{ + content-align: center top; +} + +.app-title{ + dock: top; + height: 3; + content-align: center middle; + text-style: bold; + color: $accent; +} + +.actions-buttons{ + height: auto; + margin: 1 0; + align: center middle; +} + +.tips{ + dock: bottom; + height: 4; + content-align: center middle; + color: $text-muted; +} \ No newline at end of file diff --git a/src/pilgrim/ui/ui.py b/src/pilgrim/ui/ui.py index bf8c7ed..3463197 100644 --- a/src/pilgrim/ui/ui.py +++ b/src/pilgrim/ui/ui.py @@ -1,9 +1,21 @@ +from pathlib import Path + from textual.app import App from pilgrim.service.servicemanager import ServiceManager +from pilgrim.ui.screens.diary_list_screen import DiaryListScreen + +CSS_FILE_PATH = Path(__file__).parent / "styles" / "pilgrim.css" class UIApp(App): + CSS_PATH = CSS_FILE_PATH + def __init__(self,service_manager: ServiceManager): super().__init__() self.service_manager = service_manager + + + def on_mount(self) -> None: + """Chamado quando a app inicia. Carrega a tela principal.""" + self.push_screen(DiaryListScreen()) From 821845530a9f84716bc3ce94604ed0823bf98955 Mon Sep 17 00:00:00 2001 From: Gustavo Henrique Santos Souza de Miranda Date: Mon, 9 Jun 2025 19:47:45 -0300 Subject: [PATCH 03/11] Changed some imports in photo_service.pyto avoid circular import error, and removed a package on diary_list_screen.py that is not being used. --- .idea/workspace.xml | 41 ++++++++++----------- src/pilgrim/service/photo_service.py | 3 +- src/pilgrim/ui/screens/diary_list_screen.py | 1 - 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/.idea/workspace.xml b/.idea/workspace.xml index a012a27..cd36295 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -6,10 +6,8 @@ - - - - + + - { - "keyToString": { - "ModuleVcsDetector.initialDetectionPerformed": "true", - "Python.Database.executor": "Run", - "Python.command.executor": "Run", - "Python.main.executor": "Run", - "Python.pilgrim.executor": "Run", - "RunOnceActivity.ShowReadmeOnStart": "true", - "RunOnceActivity.git.unshallow": "true", - "git-widget-placeholder": "master", - "node.js.detected.package.eslint": "true", - "node.js.detected.package.tslint": "true", - "node.js.selected.package.eslint": "(autodetect)", - "node.js.selected.package.tslint": "(autodetect)", - "nodejs_package_manager_path": "npm", - "vue.rearranger.settings.migration": "true" + +}]]> @@ -117,6 +115,7 @@ 1748985568579 +