mirror of https://github.com/gmbrax/Pilgrim.git
Added the edit_entry_screen.py and modified the mock entry_service_mock.py to be async and also add the screen change on diary_list_screen.py and added the rename_entry_modal.py and added all the necessary CSS on the pilgrim.css
This commit is contained in:
parent
981b38f994
commit
58807460d7
|
|
@ -1,2 +1,3 @@
|
||||||
database.db
|
database.db
|
||||||
__pycache__
|
__pycache__
|
||||||
|
/.idea/
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
from typing import List
|
from typing import List, Tuple
|
||||||
|
import asyncio
|
||||||
from pilgrim.service.entry_service import EntryService
|
from pilgrim.service.entry_service import EntryService
|
||||||
from pilgrim.models.entry import Entry
|
from pilgrim.models.entry import Entry
|
||||||
|
|
||||||
|
|
@ -15,23 +16,61 @@ class EntryServiceMock(EntryService):
|
||||||
travel_diary_id=1, id=2,photos=[]),
|
travel_diary_id=1, id=2,photos=[]),
|
||||||
3: Entry(title="The Mount Royal", text="The Mount Royal is fucking awesome", date="28/07/2025",
|
3: Entry(title="The Mount Royal", text="The Mount Royal is fucking awesome", date="28/07/2025",
|
||||||
travel_diary_id=1, id=3, photos=[]),
|
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:
|
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)
|
new_entry = Entry(title, text, date, travel_diary_id, id=self._next_id)
|
||||||
self.mock_data[self._next_id] = new_entry
|
self.mock_data[self._next_id] = new_entry
|
||||||
self._next_id += 1
|
self._next_id += 1
|
||||||
return new_entry
|
return new_entry
|
||||||
|
|
||||||
def read_by_id(self, entry_id: int) -> Entry | None:
|
def read_by_id(self, entry_id: int) -> Entry | None:
|
||||||
|
"""Versão síncrona"""
|
||||||
return self.mock_data.get(entry_id)
|
return self.mock_data.get(entry_id)
|
||||||
|
|
||||||
def read_all(self) -> List[Entry]:
|
def read_all(self) -> List[Entry]:
|
||||||
|
"""Versão síncrona"""
|
||||||
return list(self.mock_data.values())
|
return list(self.mock_data.values())
|
||||||
|
|
||||||
def update(self, entry_id: int, entry_dst: Entry) -> Entry | None:
|
def read_by_travel_diary_id(self, travel_diary_id: int) -> List[Entry]:
|
||||||
item_to_update = self.mock_data.get(entry_id)
|
"""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:
|
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.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.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 item_to_update
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def delete(self, entry_id: int) -> Entry | None:
|
def delete(self, entry_src: Entry) -> Entry | None:
|
||||||
return self.mock_data.pop(entry_id, 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)
|
||||||
|
|
@ -11,6 +11,7 @@ from pilgrim.models.travel_diary import TravelDiary
|
||||||
from pilgrim.ui.screens.about_screen import AboutScreen
|
from pilgrim.ui.screens.about_screen import AboutScreen
|
||||||
from pilgrim.ui.screens.edit_diary_modal import EditDiaryModal
|
from pilgrim.ui.screens.edit_diary_modal import EditDiaryModal
|
||||||
from pilgrim.ui.screens.new_diary_modal import NewDiaryModal
|
from pilgrim.ui.screens.new_diary_modal import NewDiaryModal
|
||||||
|
from pilgrim.ui.screens.edit_entry_screen import EditEntryScreen
|
||||||
|
|
||||||
|
|
||||||
class DiaryListScreen(Screen):
|
class DiaryListScreen(Screen):
|
||||||
|
|
@ -245,7 +246,11 @@ class DiaryListScreen(Screen):
|
||||||
"""Ação para abrir diário selecionado"""
|
"""Ação para abrir diário selecionado"""
|
||||||
if self.selected_diary_index is not None:
|
if self.selected_diary_index is not None:
|
||||||
diary_id = self.diary_id_map.get(self.selected_diary_index)
|
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:
|
else:
|
||||||
self.notify("Selecione um diário para abrir")
|
self.notify("Selecione um diário para abrir")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -170,3 +170,105 @@ Screen.-modal {
|
||||||
margin: 0 1;
|
margin: 0 1;
|
||||||
width: 1fr;
|
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;
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ from textual.screen import Screen
|
||||||
from pilgrim.service.servicemanager import ServiceManager
|
from pilgrim.service.servicemanager import ServiceManager
|
||||||
from pilgrim.ui.screens.about_screen import AboutScreen
|
from pilgrim.ui.screens.about_screen import AboutScreen
|
||||||
from pilgrim.ui.screens.diary_list_screen import DiaryListScreen
|
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"
|
CSS_FILE_PATH = Path(__file__).parent / "styles" / "pilgrim.css"
|
||||||
|
|
||||||
|
|
@ -42,7 +43,12 @@ class UIApp(App):
|
||||||
screen.dismiss
|
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
|
# Always include quit command
|
||||||
yield SystemCommand(
|
yield SystemCommand(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue