Compare commits

..

No commits in common. "dc8b5016f38fd500ef18c7afea735c4fce61cdd7" and "821845530a9f84716bc3ce94604ed0823bf98955" have entirely different histories.

6 changed files with 95 additions and 364 deletions

View File

@ -4,6 +4,3 @@ from pilgrim.application import Application
def main():
app = Application()
app.run()
if __name__ == "__main__":
main()

View File

@ -8,16 +8,12 @@ from pilgrim.service.servicemanager import ServiceManager
class ServiceManagerMock(ServiceManager):
def __init__(self):
super().__init__()
# Cria instâncias únicas para manter estado consistente
self._travel_diary_service = TravelDiaryServiceMock()
self._entry_service = EntryServiceMock()
self._photo_service = PhotoServiceMock()
def get_entry_service(self):
return self._entry_service
return EntryServiceMock()
def get_travel_diary_service(self):
return self._travel_diary_service
return TravelDiaryServiceMock()
def get_photo_service(self):
return self._photo_service
return PhotoServiceMock()

View File

@ -1,67 +1,34 @@
from pilgrim.service.travel_diary_service import TravelDiaryService
from pilgrim.models.travel_diary import TravelDiary
import asyncio
class TravelDiaryServiceMock(TravelDiaryService):
def __init__(self):
super().__init__(None)
self.mock_data = {
1: TravelDiary(id=1, name="Montreal"),
2: TravelDiary(id=2, name="Rio de Janeiro"),
1:TravelDiary(id=1,name="Montreal"),
2:TravelDiary(id=2,name="Rio de Janeiro"),
}
self._next_id = 3
# Métodos síncronos (originais)
def create(self, name: str):
"""Versão síncrona"""
new_travel_diary = TravelDiary(id=self._next_id, name=name)
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):
"""Versão síncrona"""
return self.mock_data.get(travel_id)
return self.mock_data[travel_id]
def read_all(self):
"""Versão síncrona"""
return list(self.mock_data.values())
def update(self, travel_diary_id: int, name: str):
"""Versão síncrona"""
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 = name
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):
"""Versão síncrona"""
return self.mock_data.pop(travel_diary_id, None)
# Métodos assíncronos (novos)
async def async_create(self, name: str):
"""Versão assíncrona"""
await asyncio.sleep(0.01) # Simula I/O
return self.create(name)
async def async_read_by_id(self, travel_id: int):
"""Versão assíncrona"""
await asyncio.sleep(0.01) # Simula I/O
return self.read_by_id(travel_id)
async def async_read_all(self):
"""Versão assíncrona"""
await asyncio.sleep(0.01) # Simula I/O
return self.read_all()
async def async_update(self, travel_diary_id: int, name: str):
"""Versão assíncrona"""
await asyncio.sleep(0.01) # Simula I/O
return self.update(travel_diary_id, name)
async def async_delete(self, travel_diary_id: int):
"""Versão assíncrona"""
await asyncio.sleep(0.01) # Simula I/O
return self.delete(travel_diary_id)

View File

@ -1,286 +1,146 @@
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", "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"),
Binding("n", "new_diary", "Novo Diário"),
Binding("^q", "quit", "Sair"),
]
def __init__(self):
super().__init__()
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"
)
self.selected_diary_index = None # Armazena o índice do diário selecionado
def compose(self) -> ComposeResult:
yield self.header
yield self.container
yield self.footer
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:
# Usa versão síncrona para o mount inicial
self.refresh_diaries()
self.update_buttons_state()
self.update_buttons_state() # Atualiza estado inicial dos botões
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:
self.diary_list.add_option("[dim]Nenhum diário encontrado. Pressione 'N' para criar um novo![/dim]")
self.selected_diary_index = None
# 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:
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()
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(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()
self.notify("Error: " + str(e))
def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None:
"""Handle quando uma opção é selecionada"""
if self.diary_id_map and event.option_index in self.diary_id_map:
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
self.action_open_diary()
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"""
has_selection = (self.selected_diary_index is not None and
self.selected_diary_index in self.diary_id_map)
"""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
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_selected_diary()
elif button_id == "open_diary":
self.action_open_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.app.push_screen(NewDiaryModal(),self._on_new_diary_submitted)
self.notify("Criando novo diário...")
# Aqui você pode navegar para uma tela de criação de diário
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):
def action_edit_diary(self):
"""Ação para editar diário selecionado"""
if self.selected_diary_index is not None:
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")
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 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"""
def refresh_diaries(self):
"""Atualiza a lista de diários no OptionList"""
try:
service = self.app.service_manager.get_travel_diary_service()
updated_diary = await service.async_update(diary_id, name)
service_manager = self.app.service_manager
option_list = self.query_one("#option-list") # Usando ID em vez de classe
if updated_diary:
self.notify(f"Diário '{name}' atualizado!")
# Força refresh após a atualização
await self.async_refresh_diaries()
# 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:
self.notify("Erro: Diário não encontrado")
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(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()
self.notify("Error no refresh_diaries: " + str(e))

View File

@ -1,48 +0,0 @@
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)

View File

@ -1,41 +0,0 @@
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("")