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
|
||||
__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.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)
|
||||
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)
|
||||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in New Issue