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] 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())