mirror of https://github.com/gmbrax/Pilgrim.git
452 lines
17 KiB
Python
452 lines
17 KiB
Python
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"
|
|
|
|
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")
|
|
]
|
|
|
|
def __init__(self, diary_id: int = 1):
|
|
super().__init__()
|
|
self.diary_id = diary_id
|
|
self.diary_name = f"Diary {diary_id}" # Use a better default name
|
|
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"""
|
|
# First update diary info, then refresh entries
|
|
self.update_diary_info()
|
|
self.refresh_entries()
|
|
|
|
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}")
|
|
else:
|
|
# If diary not found, try to get a default name
|
|
self.diary_name = f"Diary {self.diary_id}"
|
|
self.diary_info.update(f"Diary: {self.diary_name}")
|
|
self.notify(f"Diary {self.diary_id} not found, using default name")
|
|
except Exception as e:
|
|
# If there's an error, use a default name but don't break the app
|
|
self.diary_name = f"Diary {self.diary_id}"
|
|
self.diary_info.update(f"Diary: {self.diary_name}")
|
|
self.notify(f"Error loading diary info: {str(e)}")
|
|
|
|
# Always ensure the diary info is updated
|
|
self._ensure_diary_info_updated()
|
|
|
|
def _ensure_diary_info_updated(self):
|
|
"""Ensures the diary info widget is always updated with current diary name"""
|
|
try:
|
|
self.diary_info.update(f"Diary: {self.diary_name}")
|
|
except Exception as e:
|
|
# If even this fails, at least try to show something
|
|
self.diary_info.update(f"Diary: {self.diary_id}")
|
|
|
|
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)}")
|
|
|
|
# Ensure diary info is updated even if entries fail to load
|
|
self._ensure_diary_info_updated()
|
|
|
|
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) |