diff --git a/src/pilgrim/ui/screens/diary_list_screen.py b/src/pilgrim/ui/screens/diary_list_screen.py index 990d121..53abcba 100644 --- a/src/pilgrim/ui/screens/diary_list_screen.py +++ b/src/pilgrim/ui/screens/diary_list_screen.py @@ -1,146 +1,286 @@ +from typing import Optional, Tuple +import asyncio + 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 +from pilgrim.models.travel_diary import TravelDiary +from pilgrim.ui.screens.edit_diary_modal import EditDiaryModal +from pilgrim.ui.screens.new_diary_modal import NewDiaryModal + class DiaryListScreen(Screen): TITLE = "Pilgrim - Main" BINDINGS = [ - Binding("n", "new_diary", "Novo Diário"), - Binding("^q", "quit", "Sair"), + Binding("n", "new_diary", "New diary"), + Binding("^q", "quit", "Quit Pilgrim"), + Binding("enter", "open_selected_diary", "Open diary"), + Binding("e", "edit_selected_diary", "Edit diary"), + Binding("r", "force_refresh", "Force refresh"), ] def __init__(self): super().__init__() - self.selected_diary_index = None # Armazena o índice do diário selecionado + self.selected_diary_index = None + self.diary_id_map = {} + self.is_refreshing = False + + self.header = Header() + self.footer = Footer() + self.diary_list = OptionList(classes="DiaryListScreen-DiaryListOptions") + self.new_diary_button = Button("New diary", id="new_diary", classes="DiaryListScreen-NewDiaryButton") + self.edit_diary_button = Button("Edit diary", id="edit_diary", classes="DiaryListScreen-EditDiaryButton") + self.open_diary = Button("Open diary", id="open_diary", classes="DiaryListScreen-OpenDiaryButton") + self.buttons_grid = Horizontal( + self.new_diary_button, self.edit_diary_button, self.open_diary, + classes="DiaryListScreen-ButtonsGrid" + ) + self.tips = Static( + "Tip: use ↑↓ to navigate • ENTER to Select • " + "TAB to alternate the fields • SHIFT + TAB to alternate back • " + "Ctrl+P for command palette • R to force refresh", + classes="DiaryListScreen-DiaryListTips" + ) + self.container = Container( + self.diary_list, self.buttons_grid, self.tips, + classes="DiaryListScreen-DiaryListContainer" + ) 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() + yield self.header + yield self.container + yield self.footer def on_mount(self) -> None: + # Usa versão síncrona para o mount inicial self.refresh_diaries() - self.update_buttons_state() # Atualiza estado inicial dos botões + self.update_buttons_state() def refresh_diaries(self): + """Versão síncrona do refresh""" 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() + + # Usa método síncrono diaries = travel_diary_service.read_all() + # Salva o estado atual + current_diary_id = None + if (self.selected_diary_index is not None and + self.selected_diary_index in self.diary_id_map): + current_diary_id = self.diary_id_map[self.selected_diary_index] + + # Limpa e reconstrói + self.diary_list.clear_options() + self.diary_id_map = {} + 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]") + self.diary_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: - # Adiciona cada opção como string com markup rich - option_list.add_option(f"[b]{diary.name}[/b]\n[dim]ID: {diary.id}[/dim]") + new_selected_index = 0 + + for index, diary in enumerate(diaries): + self.diary_id_map[index] = diary.id + self.diary_list.add_option(f"[b]{diary.name}[/b]\n[dim]ID: {diary.id}[/dim]") + + # Mantém a seleção se possível + if current_diary_id and diary.id == current_diary_id: + new_selected_index = index + + self.selected_diary_index = new_selected_index + + # Atualiza o highlight + self.set_timer(0.05, lambda: self._update_highlight(new_selected_index)) + + # Força refresh visual + self.diary_list.refresh() + self.update_buttons_state() except Exception as e: - self.notify("Error: " + str(e)) + self.notify(f"Erro ao carregar diários: {str(e)}") + + def _update_highlight(self, index: int): + """Atualiza o highlight do OptionList""" + try: + if index < len(self.diary_list.options): + self.diary_list.highlighted = index + self.diary_list.refresh() + except Exception as e: + self.notify(f"Erro ao atualizar highlight: {str(e)}") + + async def async_refresh_diaries(self): + """Versão assíncrona do refresh""" + if self.is_refreshing: + return + + self.is_refreshing = True + + try: + service_manager = self.app.service_manager + travel_diary_service = service_manager.get_travel_diary_service() + + # Usa método assíncrono + diaries = await travel_diary_service.async_read_all() + + # Salva o estado atual + current_diary_id = None + if (self.selected_diary_index is not None and + self.selected_diary_index in self.diary_id_map): + current_diary_id = self.diary_id_map[self.selected_diary_index] + + # Limpa e reconstrói + self.diary_list.clear_options() + self.diary_id_map = {} + + if not diaries: + self.diary_list.add_option("[dim]Nenhum diário encontrado. Pressione 'N' para criar um novo![/dim]") + self.selected_diary_index = None + else: + new_selected_index = 0 + + for index, diary in enumerate(diaries): + self.diary_id_map[index] = diary.id + self.diary_list.add_option(f"[b]{diary.name}[/b]\n[dim]ID: {diary.id}[/dim]") + + if current_diary_id and diary.id == current_diary_id: + new_selected_index = index + + self.selected_diary_index = new_selected_index + self.set_timer(0.05, lambda: self._update_highlight(new_selected_index)) + + self.diary_list.refresh() + self.update_buttons_state() + + except Exception as e: + self.notify(f"Erro ao carregar diários: {str(e)}") + finally: + self.is_refreshing = False + + def on_option_list_option_highlighted(self, event: OptionList.OptionHighlighted) -> None: + """Handle quando uma opção é destacada""" + if self.diary_id_map and event.option_index in self.diary_id_map: + self.selected_diary_index = event.option_index + else: + self.selected_diary_index = None + + self.update_buttons_state() 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): + if self.diary_id_map and event.option_index in self.diary_id_map: self.selected_diary_index = event.option_index - selected_diary = diaries[event.option_index] - self.notify(f"Diário selecionado: {selected_diary.name}") + self.action_open_diary() 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 + """Atualiza o estado dos botões""" + has_selection = (self.selected_diary_index is not None and + self.selected_diary_index in self.diary_id_map) + self.edit_diary_button.disabled = not has_selection + self.open_diary.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": + 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!") + elif button_id == "edit_diary": + self.action_edit_selected_diary() + elif button_id == "open_diary": + self.action_open_diary() 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 + self.app.push_screen(NewDiaryModal(),self._on_new_diary_submitted) - def action_edit_diary(self): + def _on_new_diary_submitted(self,result): + self.notify(str(result)) + if result: + self.notify(f"Creating Diary:{result}'...") + self.call_later(self._async_create_diary,result) + else: + self.notify(f"Canceled...") + + async def _async_create_diary(self,name: str): + + try: + service = self.app.service_manager.get_travel_diary_service() + created_diary = await service.async_create(name) + if created_diary: + self.diary_id_map[created_diary.id] = created_diary.id + await self.async_refresh_diaries() + self.notify(f"Diary: '{name}' created!") + else: + self.notify("Error Creating the diary") + except Exception as e: + self.notify(f"Exception on creating the diary: {str(e)}") + + + + def action_edit_selected_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)) + diary_id = self.diary_id_map.get(self.selected_diary_index) + if diary_id: + self.app.push_screen( + EditDiaryModal(diary_id=diary_id), + self._on_edited_diary_name_submitted + ) + else: + self.notify("Selecione um diário para editar") - def refresh_diaries(self): - """Atualiza a lista de diários no OptionList""" + def action_open_diary(self): + """Ação para abrir diário selecionado""" + if self.selected_diary_index is not None: + diary_id = self.diary_id_map.get(self.selected_diary_index) + self.notify(f"Abrindo diário ID: {diary_id}") + else: + self.notify("Selecione um diário para abrir") + + def _on_edited_diary_name_submitted(self, result: Optional[Tuple[int, str]]) -> None: + """Callback após edição do diário""" + if result: + diary_id, name = result + self.notify(f"Atualizando diário ID {diary_id} para '{name}'...") + # Agenda a atualização assíncrona + self.call_later(self._async_update_diary, diary_id, name) + else: + self.notify("Edição cancelada") + + async def _async_update_diary(self, diary_id: int, name: str): + """Atualiza o diário de forma assíncrona""" try: - service_manager = self.app.service_manager - option_list = self.query_one("#option-list") # Usando ID em vez de classe + service = self.app.service_manager.get_travel_diary_service() + updated_diary = await service.async_update(diary_id, name) - # 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 + if updated_diary: + self.notify(f"Diário '{name}' atualizado!") + # Força refresh após a atualização + await self.async_refresh_diaries() 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 + self.notify("Erro: Diário não encontrado") except Exception as e: - self.notify("Error no refresh_diaries: " + str(e)) \ No newline at end of file + self.notify(f"Erro ao atualizar: {str(e)}") + + def action_force_refresh(self): + """Força refresh manual""" + self.notify("Forçando refresh...") + # Tenta ambas as versões + self.refresh_diaries() # Síncrona + self.call_later(self.async_refresh_diaries) # Assíncrona + + def action_open_selected_diary(self): + """Ação do binding ENTER""" + self.action_open_diary() \ No newline at end of file diff --git a/src/pilgrim/ui/screens/edit_diary_modal.py b/src/pilgrim/ui/screens/edit_diary_modal.py new file mode 100644 index 0000000..20ab157 --- /dev/null +++ b/src/pilgrim/ui/screens/edit_diary_modal.py @@ -0,0 +1,48 @@ +from textual.app import ComposeResult +from textual.containers import Vertical, Horizontal +from textual.screen import ModalScreen +from textual.widgets import Label, Input, Button + + +class EditDiaryModal(ModalScreen[tuple[int,str]]): + BINDINGS = [ + ("escape", "cancel", "Cancelar"), + ] + + def __init__(self, diary_id: int): + super().__init__() + self.diary_id = diary_id + self.current_diary_name = self.app.service_manager.get_travel_diary_service().read_by_id(self.diary_id).name + self.name_input = Input(value=self.current_diary_name, id="edit_diary_name_input") + + def compose(self) -> ComposeResult: + with Vertical(id="edit_diary_dialog"): + yield Label(f"Editar Diário: {self.current_diary_name}", classes="dialog-title") + yield Label("Novo Nome do Diário:") + yield self.name_input + with Horizontal(classes="dialog-buttons"): + yield Button("Salvar", variant="primary", id="save_diary_button") + yield Button("Cancelar", variant="default", id="cancel_button") + + def on_mount(self) -> None: + """Foca no campo de entrada e move o cursor para o final do texto.""" + self.name_input.focus() + self.name_input.cursor_position = len(self.name_input.value) + # REMOVIDA A LINHA QUE CAUSA O ERRO: self.name_input.select_text() + + def on_button_pressed(self, event: Button.Pressed) -> None: + if event.button.id == "save_diary_button": + new_diary_name = self.name_input.value.strip() + if new_diary_name and new_diary_name != self.current_diary_name: + self.dismiss((self.diary_id, new_diary_name)) + elif new_diary_name == self.current_diary_name: + self.notify("Nenhuma alteração feita.", severity="warning") + self.dismiss(None) + else: + self.notify("O nome do diário não pode estar vazio.", severity="warning") + self.name_input.focus() + elif event.button.id == "cancel_button": + self.dismiss(None) + + def action_cancel(self) -> None: + self.dismiss(None) \ No newline at end of file diff --git a/src/pilgrim/ui/screens/new_diary_modal.py b/src/pilgrim/ui/screens/new_diary_modal.py new file mode 100644 index 0000000..310f843 --- /dev/null +++ b/src/pilgrim/ui/screens/new_diary_modal.py @@ -0,0 +1,41 @@ +from textual.app import ComposeResult +from textual.containers import Vertical, Horizontal +from textual.screen import ModalScreen +from textual.widgets import Label, Input, Button + +class NewDiaryModal(ModalScreen[str]): + BINDINGS = [ + ("escape", "cancel", "Cancelar"), + ] + def __init__(self): + super().__init__() + self.name_input = Input(id="NewDiaryModal-NameInput") # This ID is fine, it's specific to the input + + def compose(self) -> ComposeResult: + # CHANGE THIS LINE: Use the ID that matches your CSS + with Vertical(id="new_diary_dialog"): # <--- Changed ID here to match CSS + yield Label("Create a new diary", classes="dialog-title") + yield Label("Diary Name:") + yield self.name_input + with Horizontal(classes="dialog-buttons"): + yield Button("Create", variant="primary", id="create_diary_button") + yield Button("Cancel", variant="default", id="cancel_button") + + def on_mount(self): + self.name_input.focus() + + def on_button_pressed(self, event: Button.Pressed) -> None: + """Lida com os cliques dos botões.""" + if event.button.id == "create_diary_button": + diary_name = self.name_input.value.strip() + if diary_name: + self.dismiss(diary_name) + else: + self.notify("O nome do diário não pode estar vazio.", severity="warning") + self.name_input.focus() + elif event.button.id == "cancel_button": + self.dismiss("") + + def action_cancel(self) -> None: + """Ação para cancelar a modal.""" + self.dismiss("") \ No newline at end of file