diff --git a/.gitignore b/.gitignore index 08fdd16..ca05f50 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ database.db __pycache__ +/.idea/ diff --git a/src/pilgrim/service/mocks/entry_service_mock.py b/src/pilgrim/service/mocks/entry_service_mock.py index a091a52..5b93f51 100644 --- a/src/pilgrim/service/mocks/entry_service_mock.py +++ b/src/pilgrim/service/mocks/entry_service_mock.py @@ -1,4 +1,5 @@ -from typing import List +from typing import List, Tuple +import asyncio from pilgrim.service.entry_service import EntryService from pilgrim.models.entry import Entry @@ -15,23 +16,61 @@ class EntryServiceMock(EntryService): 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=[]), + 4: Entry(title="Old Montreal", text="Exploring the historic district", date="29/07/2025", + travel_diary_id=1, id=4, photos=[]), + 5: Entry(title="Notre-Dame Basilica", text="Beautiful architecture", date="30/07/2025", + travel_diary_id=1, id=5, photos=[]), + 6: Entry(title="Parc Jean-Drapeau", text="Great views of the city", date="31/07/2025", + travel_diary_id=1, id=6, photos=[]), + 7: Entry(title="La Ronde", text="Amusement park fun", date="01/08/2025", + travel_diary_id=1, id=7, photos=[]), + 8: Entry(title="Biodome", text="Nature and science", date="02/08/2025", + travel_diary_id=1, id=8, photos=[]), + 9: Entry(title="Botanical Gardens", text="Peaceful walk", date="03/08/2025", + travel_diary_id=1, id=9, photos=[]), + 10: Entry(title="Olympic Stadium", text="Historic venue", date="04/08/2025", + travel_diary_id=1, id=10, photos=[]), } - self._next_id = 4 + self._next_id = 11 + # Métodos síncronos (mantidos para compatibilidade) def create(self, travel_diary_id: int, title: str, text: str, date: str) -> Entry: + """Versão síncrona""" 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: + """Versão síncrona""" return self.mock_data.get(entry_id) def read_all(self) -> List[Entry]: + """Versão síncrona""" 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) + def read_by_travel_diary_id(self, travel_diary_id: int) -> List[Entry]: + """Versão síncrona - lê entradas por diário""" + return [entry for entry in self.mock_data.values() if entry.fk_travel_diary_id == travel_diary_id] + + def read_paginated(self, travel_diary_id: int, page: int = 1, page_size: int = 5) -> Tuple[List[Entry], int, int]: + """Versão síncrona - lê entradas paginadas por diário""" + entries = self.read_by_travel_diary_id(travel_diary_id) + entries.sort(key=lambda x: x.id, reverse=True) # Mais recentes primeiro + + total_entries = len(entries) + total_pages = (total_entries + page_size - 1) // page_size + + start_index = (page - 1) * page_size + end_index = start_index + page_size + + page_entries = entries[start_index:end_index] + + return page_entries, total_pages, total_entries + + def update(self, entry_src: Entry, entry_dst: Entry) -> Entry | None: + """Versão síncrona""" + item_to_update = self.mock_data.get(entry_src.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 @@ -43,5 +82,42 @@ class EntryServiceMock(EntryService): 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 + def delete(self, entry_src: Entry) -> Entry | None: + """Versão síncrona""" + return self.mock_data.pop(entry_src.id, None) + + # Métodos assíncronos (principais) + async def async_create(self, travel_diary_id: int, title: str, text: str, date: str) -> Entry: + """Versão assíncrona""" + await asyncio.sleep(0.01) # Simula I/O + return self.create(travel_diary_id, title, text, date) + + async def async_read_by_id(self, entry_id: int) -> Entry | None: + """Versão assíncrona""" + await asyncio.sleep(0.01) # Simula I/O + return self.read_by_id(entry_id) + + async def async_read_all(self) -> List[Entry]: + """Versão assíncrona""" + await asyncio.sleep(0.01) # Simula I/O + return self.read_all() + + async def async_read_by_travel_diary_id(self, travel_diary_id: int) -> List[Entry]: + """Versão assíncrona - lê entradas por diário""" + await asyncio.sleep(0.01) # Simula I/O + return self.read_by_travel_diary_id(travel_diary_id) + + async def async_read_paginated(self, travel_diary_id: int, page: int = 1, page_size: int = 5) -> Tuple[List[Entry], int, int]: + """Versão assíncrona - lê entradas paginadas por diário""" + await asyncio.sleep(0.01) # Simula I/O + return self.read_paginated(travel_diary_id, page, page_size) + + async def async_update(self, entry_src: Entry, entry_dst: Entry) -> Entry | None: + """Versão assíncrona""" + await asyncio.sleep(0.01) # Simula I/O + return self.update(entry_src, entry_dst) + + async def async_delete(self, entry_src: Entry) -> Entry | None: + """Versão assíncrona""" + await asyncio.sleep(0.01) # Simula I/O + return self.delete(entry_src) \ No newline at end of file diff --git a/src/pilgrim/ui/screens/diary_list_screen.py b/src/pilgrim/ui/screens/diary_list_screen.py index 5f863a9..0240ba5 100644 --- a/src/pilgrim/ui/screens/diary_list_screen.py +++ b/src/pilgrim/ui/screens/diary_list_screen.py @@ -11,6 +11,7 @@ from pilgrim.models.travel_diary import TravelDiary from pilgrim.ui.screens.about_screen import AboutScreen from pilgrim.ui.screens.edit_diary_modal import EditDiaryModal from pilgrim.ui.screens.new_diary_modal import NewDiaryModal +from pilgrim.ui.screens.edit_entry_screen import EditEntryScreen class DiaryListScreen(Screen): @@ -245,7 +246,11 @@ class DiaryListScreen(Screen): """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}") + if diary_id: + self.app.push_screen(EditEntryScreen(diary_id=diary_id)) + self.notify(f"Opening diary ID: {diary_id}") + else: + self.notify("Invalid diary ID") else: self.notify("Selecione um diário para abrir") diff --git a/src/pilgrim/ui/screens/edit_entry_screen.py b/src/pilgrim/ui/screens/edit_entry_screen.py new file mode 100644 index 0000000..f1cc3d9 --- /dev/null +++ b/src/pilgrim/ui/screens/edit_entry_screen.py @@ -0,0 +1,430 @@ +from typing import Optional, List +import asyncio +from datetime import datetime + +from textual.app import ComposeResult +from textual.screen import Screen +from textual.widgets import Header, Footer, Static, TextArea +from textual.binding import Binding +from textual.containers import Container, Horizontal + +from pilgrim.models.entry import Entry +from pilgrim.models.travel_diary import TravelDiary +from pilgrim.ui.screens.rename_entry_modal import RenameEntryModal + + +class EditEntryScreen(Screen): + TITLE = "Pilgrim - Edit Entry" + + BINDINGS = [ + Binding("ctrl+s", "save", "Save"), + Binding("ctrl+n", "next_entry", "Next/New Entry"), + Binding("ctrl+b", "prev_entry", "Previous Entry"), + Binding("ctrl+r", "rename_entry", "Rename Entry"), + Binding("escape", "back_to_list", "Back to List"), + Binding("r", "force_refresh", "Force refresh"), + ] + + def __init__(self, diary_id: int = 1): + super().__init__() + self.diary_id = diary_id + self.diary_name = "Unknown Diary" + self.current_entry_index = 0 + self.entries: List[Entry] = [] + self.is_new_entry = False + self.has_unsaved_changes = False + self.new_entry_content = "" + self.new_entry_title = "New Entry" + self.next_entry_id = 1 + self._updating_display = False + self._original_content = "" + self.is_refreshing = False + + # Main header + self.header = Header(name="Pilgrim v6", classes="EditEntryScreen-header") + + # Sub-header widgets + self.diary_info = Static(f"Diary: {self.diary_name}", id="diary_info", classes="EditEntryScreen-diary-info") + self.entry_info = Static("Loading...", id="entry_info", classes="EditEntryScreen-entry-info") + self.status_indicator = Static("Saved", id="status_indicator", classes="EditEntryScreen-status-indicator") + + # Sub-header container + self.sub_header = Horizontal( + self.diary_info, + Static(classes="spacer EditEntryScreen-spacer"), + self.entry_info, + self.status_indicator, + id="sub_header", + classes="EditEntryScreen-sub-header" + ) + + # Text area + self.text_entry = TextArea(id="text_entry", classes="EditEntryScreen-text-entry") + + # Main container + self.main = Container( + self.sub_header, + self.text_entry, + id="EditEntryScreen_MainContainer", + classes="EditEntryScreen-main-container" + ) + + # Footer + self.footer = Footer(classes="EditEntryScreen-footer") + + def compose(self) -> ComposeResult: + yield self.header + yield self.main + yield self.footer + + def on_mount(self) -> None: + """Called when the screen is mounted""" + self.refresh_entries() + self.update_diary_info() + + def update_diary_info(self): + """Updates diary information""" + try: + service_manager = self.app.service_manager + travel_diary_service = service_manager.get_travel_diary_service() + + diary = travel_diary_service.read_by_id(self.diary_id) + if diary: + self.diary_name = diary.name + self.diary_info.update(f"Diary: {self.diary_name}") + except Exception as e: + self.notify(f"Error loading diary info: {str(e)}") + + def refresh_entries(self): + """Synchronous version of refresh""" + try: + service_manager = self.app.service_manager + entry_service = service_manager.get_entry_service() + + # Get all entries for this diary + all_entries = entry_service.read_all() + self.entries = [entry for entry in all_entries if entry.fk_travel_diary_id == self.diary_id] + + # Sort by ID + self.entries.sort(key=lambda x: x.id) + + # Update next entry ID + if self.entries: + self.next_entry_id = max(entry.id for entry in self.entries) + 1 + else: + self.next_entry_id = 1 + + self._update_entry_display() + self._update_sub_header() + + except Exception as e: + self.notify(f"Error loading entries: {str(e)}") + + async def async_refresh_entries(self): + """Asynchronous version of refresh""" + if self.is_refreshing: + return + + self.is_refreshing = True + + try: + service_manager = self.app.service_manager + entry_service = service_manager.get_entry_service() + + # For now, use synchronous method since mock doesn't have async + all_entries = entry_service.read_all() + self.entries = [entry for entry in all_entries if entry.fk_travel_diary_id == self.diary_id] + + # Sort by ID + self.entries.sort(key=lambda x: x.id) + + # Update next entry ID + if self.entries: + self.next_entry_id = max(entry.id for entry in self.entries) + 1 + else: + self.next_entry_id = 1 + + self._update_entry_display() + self._update_sub_header() + + except Exception as e: + self.notify(f"Error loading entries: {str(e)}") + finally: + self.is_refreshing = False + + def _update_status_indicator(self, text: str, css_class: str): + """Helper to update status indicator text and class.""" + self.status_indicator.update(text) + self.status_indicator.remove_class("saved", "not-saved", "new", "read-only") + self.status_indicator.add_class(css_class) + + def _update_sub_header(self): + """Updates the sub-header with current entry information.""" + if not self.entries and not self.is_new_entry: + self.entry_info.update("No entries") + self._update_status_indicator("Saved", "saved") + return + + if self.is_new_entry: + self.entry_info.update(f"New Entry: {self.new_entry_title}") + if self.has_unsaved_changes: + self._update_status_indicator("Not Saved", "not-saved") + else: + self._update_status_indicator("New", "new") + else: + current_entry = self.entries[self.current_entry_index] + entry_text = f"Entry: ({self.current_entry_index + 1}/{len(self.entries)}) {current_entry.title}" + self.entry_info.update(entry_text) + self._update_status_indicator("Saved", "saved") + + def _save_current_state(self): + """Saves the current state before navigating""" + if self.is_new_entry: + self.new_entry_content = self.text_entry.text + elif self.entries and self.has_unsaved_changes: + current_entry = self.entries[self.current_entry_index] + current_entry.text = self.text_entry.text + + def _finish_display_update(self): + """Finishes the display update by reactivating change detection""" + self._updating_display = False + self._update_sub_header() + + def _update_entry_display(self): + """Updates the display of the current entry""" + if not self.entries and not self.is_new_entry: + self.text_entry.text = f"No entries found for diary '{self.diary_name}'\n\nPress Ctrl+N to create a new entry." + self.text_entry.read_only = True + self._original_content = self.text_entry.text + self._update_sub_header() + return + + self._updating_display = True + + if self.is_new_entry: + self.text_entry.text = self.new_entry_content + self.text_entry.read_only = False + self._original_content = self.new_entry_content + self.has_unsaved_changes = False + else: + current_entry = self.entries[self.current_entry_index] + self.text_entry.text = current_entry.text + self.text_entry.read_only = False + self._original_content = current_entry.text + self.has_unsaved_changes = False + + self.call_after_refresh(self._finish_display_update) + + def on_text_area_changed(self, event) -> None: + """Detects text changes to mark as unsaved""" + if (hasattr(self, 'text_entry') and not self.text_entry.read_only and + not getattr(self, '_updating_display', False) and hasattr(self, '_original_content')): + current_content = self.text_entry.text + if current_content != self._original_content: + if not self.has_unsaved_changes: + self.has_unsaved_changes = True + self._update_sub_header() + else: + if self.has_unsaved_changes: + self.has_unsaved_changes = False + self._update_sub_header() + + def action_back_to_list(self) -> None: + """Goes back to the diary list""" + if self.is_new_entry and not self.text_entry.text.strip() and not self.has_unsaved_changes: + self.app.pop_screen() + self.notify("Returned to diary list") + return + + if self.has_unsaved_changes or (self.is_new_entry and self.text_entry.text.strip()): + self.notify("There are unsaved changes! Use Ctrl+S to save before leaving.") + return + + self.app.pop_screen() + self.notify("Returned to diary list") + + def action_next_entry(self) -> None: + """Goes to the next entry""" + self._save_current_state() + + if not self.entries: + if not self.is_new_entry: + self.is_new_entry = True + self._update_entry_display() + self.notify("New entry created") + else: + self.notify("Already in a new entry") + return + + if self.is_new_entry: + self.notify("Already at the last position (new entry)") + elif self.current_entry_index < len(self.entries) - 1: + self.current_entry_index += 1 + self._update_entry_display() + current_entry = self.entries[self.current_entry_index] + self.notify(f"Navigating to: {current_entry.title}") + else: + self.is_new_entry = True + self._update_entry_display() + self.notify("New entry created") + + def action_prev_entry(self) -> None: + """Goes to the previous entry""" + self._save_current_state() + + if not self.entries: + self.notify("No entries to navigate") + return + + if self.is_new_entry: + if self.entries: + self.is_new_entry = False + self.current_entry_index = len(self.entries) - 1 + self._update_entry_display() + current_entry = self.entries[self.current_entry_index] + self.notify(f"Navigating to: {current_entry.title}") + else: + self.notify("No previous entries") + elif self.current_entry_index > 0: + self.current_entry_index -= 1 + self._update_entry_display() + current_entry = self.entries[self.current_entry_index] + self.notify(f"Navigating to: {current_entry.title}") + else: + self.notify("Already at the first entry") + + def action_rename_entry(self) -> None: + """Opens a modal to rename the entry.""" + if not self.entries and not self.is_new_entry: + self.notify("No entry to rename", severity="warning") + return + + if self.is_new_entry: + current_name = self.new_entry_title + else: + current_entry = self.entries[self.current_entry_index] + current_name = current_entry.title + + self.app.push_screen( + RenameEntryModal(current_name=current_name), + self.handle_rename_result + ) + + def handle_rename_result(self, new_name: str | None) -> None: + """Callback that processes the rename modal result.""" + if new_name is None: + self.notify("Rename cancelled") + return + + if not new_name.strip(): + self.notify("Name cannot be empty", severity="error") + return + + if self.is_new_entry: + old_name = self.new_entry_title + self.new_entry_title = new_name + self.notify(f"New entry title changed to '{new_name}'") + else: + current_entry = self.entries[self.current_entry_index] + old_name = current_entry.title + current_entry.title = new_name + self.notify(f"Title changed from '{old_name}' to '{new_name}'") + + self.has_unsaved_changes = True + self._update_sub_header() + + def action_save(self) -> None: + """Saves the current entry""" + if self.is_new_entry: + content = self.text_entry.text.strip() + if not content: + self.notify("Empty entry cannot be saved") + return + + # Schedule async creation + self.call_later(self._async_create_entry, content) + else: + # Schedule async update + self.call_later(self._async_update_entry) + + async def _async_create_entry(self, content: str): + """Creates a new entry asynchronously""" + try: + service_manager = self.app.service_manager + entry_service = service_manager.get_entry_service() + + # Get current date + current_date = datetime.now().strftime("%d/%m/%Y") + + new_entry = entry_service.create( + travel_diary_id=self.diary_id, + title=self.new_entry_title, + text=content, + date=current_date + ) + + if new_entry: + self.entries.append(new_entry) + self.entries.sort(key=lambda x: x.id) + + # Find the new entry index + for i, entry in enumerate(self.entries): + if entry.id == new_entry.id: + self.current_entry_index = i + break + + self.is_new_entry = False + self.has_unsaved_changes = False + self._original_content = new_entry.text + self.new_entry_title = "New Entry" + self.next_entry_id = max(entry.id for entry in self.entries) + 1 + + self._update_entry_display() + self.notify(f"New entry '{new_entry.title}' saved successfully!") + else: + self.notify("Error creating entry") + + except Exception as e: + self.notify(f"Error creating entry: {str(e)}") + + async def _async_update_entry(self): + """Updates the current entry asynchronously""" + try: + if not self.entries: + self.notify("No entry to update") + return + + current_entry = self.entries[self.current_entry_index] + updated_content = self.text_entry.text + + # Create updated entry object + updated_entry = Entry( + title=current_entry.title, + text=updated_content, + date=current_entry.date, + travel_diary_id=current_entry.fk_travel_diary_id, + id=current_entry.id + ) + + service_manager = self.app.service_manager + entry_service = service_manager.get_entry_service() + + result = entry_service.update(current_entry, updated_entry) + + if result: + current_entry.text = updated_content + self.has_unsaved_changes = False + self._original_content = updated_content + self._update_sub_header() + self.notify(f"Entry '{current_entry.title}' saved successfully!") + else: + self.notify("Error updating entry") + + except Exception as e: + self.notify(f"Error updating entry: {str(e)}") + + def action_force_refresh(self): + """Forces manual refresh""" + self.notify("Forcing refresh...") + self.refresh_entries() + self.call_later(self.async_refresh_entries) \ No newline at end of file diff --git a/src/pilgrim/ui/screens/rename_entry_modal.py b/src/pilgrim/ui/screens/rename_entry_modal.py new file mode 100644 index 0000000..ae42671 --- /dev/null +++ b/src/pilgrim/ui/screens/rename_entry_modal.py @@ -0,0 +1,58 @@ +from textual.app import ComposeResult +from textual.containers import Vertical, Horizontal +from textual.screen import ModalScreen +from textual.widgets import Label, Input, Button + + +class RenameEntryModal(ModalScreen[str]): + """A modal screen to rename a diary entry.""" + + BINDINGS = [ + ("escape", "cancel", "Cancel"), + ] + + def __init__(self, current_name: str): + super().__init__() + self._current_name = current_name + self.name_input = Input( + value=self._current_name, + placeholder="Type the new name...", + id="rename_input", + classes="RenameEntryModal-name-input" + ) + + def compose(self) -> ComposeResult: + with Vertical(id="rename_entry_dialog", classes="RenameEntryModal-dialog"): + yield Label("Rename Entry", classes="dialog-title RenameEntryModal-title") + yield Label("New Entry Title:", classes="RenameEntryModal-label") + yield self.name_input + with Horizontal(classes="dialog-buttons RenameEntryModal-buttons"): + yield Button("Save", variant="primary", id="save", classes="RenameEntryModal-save-button") + yield Button("Cancel", variant="default", id="cancel", classes="RenameEntryModal-cancel-button") + + def on_mount(self) -> None: + """Focuses on the input when the screen is mounted.""" + self.name_input.focus() + self.name_input.cursor_position = len(self.name_input.value) + + def on_button_pressed(self, event: Button.Pressed) -> None: + """Handles button clicks.""" + if event.button.id == "save": + new_name = self.name_input.value.strip() + if new_name: + self.dismiss(new_name) # Returns the new name + else: + self.dismiss(None) # Considers empty name as cancellation + else: + self.dismiss(None) # Returns None for cancellation + + def on_input_submitted(self, event: Input.Submitted) -> None: + """Allows saving by pressing Enter.""" + new_name = event.value.strip() + if new_name: + self.dismiss(new_name) + else: + self.dismiss(None) + + def action_cancel(self) -> None: + self.dismiss(None) \ No newline at end of file diff --git a/src/pilgrim/ui/styles/pilgrim.css b/src/pilgrim/ui/styles/pilgrim.css index 6cc75cc..673bbe5 100644 --- a/src/pilgrim/ui/styles/pilgrim.css +++ b/src/pilgrim/ui/styles/pilgrim.css @@ -169,4 +169,106 @@ Screen.-modal { .NewDiaryModal-ButtonsContainer Button{ margin: 0 1; width: 1fr; +} + +/* EditEntryScreen Styles */ +.EditEntryScreen-sub-header { + layout: horizontal; + background: $primary-darken-1; + height: 1; + padding: 0 1; + align: center middle; + width: 100%; +} + +.EditEntryScreen-diary-info { + width: auto; + color: $text-muted; + margin-right: 2; +} + +.EditEntryScreen-entry-info { + width: auto; + color: $text; + margin-right: 1; +} + +.EditEntryScreen-spacer { + width: 1fr; +} + +.EditEntryScreen-status-indicator { + width: auto; + padding: 0 1; + content-align: center middle; +} + +.EditEntryScreen-status-indicator.saved { + background: #2E8B57; /* SeaGreen */ + color: $text; +} + +.EditEntryScreen-status-indicator.not-saved { + background: #B22222; /* FireBrick */ + color: $text; +} + +.EditEntryScreen-status-indicator.new { + background: #4682B4; /* SteelBlue */ + color: $text; +} + +.EditEntryScreen-status-indicator.read-only { + background: #696969; /* DimGray */ + color: $text-muted; +} + +.EditEntryScreen-main-container { + background: bisque; + margin: 0; +} + +.EditEntryScreen-text-entry { + width: 100%; + height: 1fr; + border: none; +} + +/* RenameEntryModal Styles */ +.RenameEntryModal-dialog { + layout: vertical; + width: 60%; + height: auto; + background: $surface; + border: thick $accent; + padding: 2 4; + align: center middle; +} + +.RenameEntryModal-title { + text-align: center; + text-style: bold; + color: $accent; + margin-bottom: 1; +} + +.RenameEntryModal-label { + margin-bottom: 1; +} + +.RenameEntryModal-name-input { + width: 1fr; + margin-bottom: 2; +} + +.RenameEntryModal-buttons { + width: 1fr; + height: auto; + align: center middle; + padding-top: 1; +} + +.RenameEntryModal-buttons Button { + margin: 0 1; + width: 1fr; } \ No newline at end of file diff --git a/src/pilgrim/ui/ui.py b/src/pilgrim/ui/ui.py index cb6aae1..ecbdeb2 100644 --- a/src/pilgrim/ui/ui.py +++ b/src/pilgrim/ui/ui.py @@ -8,6 +8,7 @@ from textual.screen import Screen from pilgrim.service.servicemanager import ServiceManager from pilgrim.ui.screens.about_screen import AboutScreen from pilgrim.ui.screens.diary_list_screen import DiaryListScreen +from pilgrim.ui.screens.edit_entry_screen import EditEntryScreen CSS_FILE_PATH = Path(__file__).parent / "styles" / "pilgrim.css" @@ -42,7 +43,12 @@ class UIApp(App): screen.dismiss ) - + elif isinstance(screen, EditEntryScreen): + yield SystemCommand( + "Back to Diary List", + "Return to the diary list", + screen.action_back_to_list + ) # Always include quit command yield SystemCommand(