From f84da2c9341d39568678ae14bb00fefb1c42851d Mon Sep 17 00:00:00 2001 From: Gustavo Henrique Santos Souza de Miranda Date: Sun, 29 Jun 2025 21:38:33 -0300 Subject: [PATCH 1/3] Improved the photo sidebar and add the add_photo_modal.py to add photos and the file_picker_modal.py to select the proper file to be ingested, also changed photo.py and photo_service.py to receive a datatime and not a string representing the addition date --- .gitignore | 3 + src/pilgrim/application.py | 1 - src/pilgrim/database.py | 7 +- src/pilgrim/models/photo.py | 20 +++-- .../service/mocks/photo_service_mock.py | 26 +++--- src/pilgrim/service/photo_service.py | 44 ++++++++-- src/pilgrim/ui/screens/edit_entry_screen.py | 85 ++++++++++++++----- .../ui/screens/modals/add_photo_modal.py | 52 +++++++++++- .../ui/screens/modals/file_picker_modal.py | 8 +- src/pilgrim/ui/styles/pilgrim.css | 38 +++++++++ 10 files changed, 226 insertions(+), 58 deletions(-) diff --git a/.gitignore b/.gitignore index ca05f50..4f61e8d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ database.db __pycache__ /.idea/ +.build-vend +.venv +dist_nuitka \ No newline at end of file diff --git a/src/pilgrim/application.py b/src/pilgrim/application.py index abf7b68..674cc0c 100644 --- a/src/pilgrim/application.py +++ b/src/pilgrim/application.py @@ -1,5 +1,4 @@ from pilgrim.database import Database -from pilgrim.service.mocks.service_manager_mock import ServiceManagerMock from pilgrim.service.servicemanager import ServiceManager from pilgrim.ui.ui import UIApp diff --git a/src/pilgrim/database.py b/src/pilgrim/database.py index 389b15b..b940c54 100644 --- a/src/pilgrim/database.py +++ b/src/pilgrim/database.py @@ -12,10 +12,13 @@ class Database: echo=False, connect_args={"check_same_thread": False}, ) - self.session = sessionmaker(bind=self.engine, autoflush=False, autocommit=False) + self._session_maker = sessionmaker(bind=self.engine, autoflush=False, autocommit=False) def create(self): Base.metadata.create_all(self.engine) + def session(self): + return self._session_maker() + def get_db(self): - return self.session() + return self._session_maker() diff --git a/src/pilgrim/models/photo.py b/src/pilgrim/models/photo.py index f6e0406..a164474 100644 --- a/src/pilgrim/models/photo.py +++ b/src/pilgrim/models/photo.py @@ -1,6 +1,8 @@ from typing import Any +from datetime import datetime +from pathlib import Path -from sqlalchemy import Column, Integer, String, ForeignKey +from sqlalchemy import Column, Integer, String, ForeignKey, DateTime from sqlalchemy.orm import relationship from pilgrim.models.photo_in_entry import photo_entry_association @@ -12,7 +14,7 @@ class Photo(Base): id = Column(Integer, primary_key=True) filepath = Column(String) name = Column(String) - addition_date = Column(String) + addition_date = Column(DateTime, default=datetime.now) caption = Column(String) entries = relationship( "Entry", @@ -22,10 +24,16 @@ class Photo(Base): fk_travel_diary_id = Column(Integer, ForeignKey("travel_diaries.id"),nullable=False) - def __init__(self, filepath, name, addition_date=None, caption=None, entries=None, **kw: Any): + def __init__(self, filepath, name, addition_date=None, caption=None, entries=None, fk_travel_diary_id=None, **kw: Any): super().__init__(**kw) - self.filepath = filepath + # Convert Path to string if needed + if isinstance(filepath, Path): + self.filepath = str(filepath) + else: + self.filepath = filepath self.name = name - self.addition_date = addition_date + self.addition_date = addition_date if addition_date is not None else datetime.now() self.caption = caption - self.entries = entries + self.entries = entries if entries is not None else [] + if fk_travel_diary_id is not None: + self.fk_travel_diary_id = fk_travel_diary_id diff --git a/src/pilgrim/service/mocks/photo_service_mock.py b/src/pilgrim/service/mocks/photo_service_mock.py index 1b02c56..0f9078a 100644 --- a/src/pilgrim/service/mocks/photo_service_mock.py +++ b/src/pilgrim/service/mocks/photo_service_mock.py @@ -12,7 +12,14 @@ class PhotoServiceMock(PhotoService): self._next_id = 1 def create(self, filepath: Path, name: str, travel_diary_id, addition_date=None, caption=None) -> Photo | None: - new_photo = Photo(filepath, name, addition_date=addition_date, caption=caption) + new_photo = Photo( + filepath=filepath, + name=name, + addition_date=addition_date, + caption=caption, + fk_travel_diary_id=travel_diary_id + ) + new_photo.id = self._next_id self.mock_data[self._next_id] = new_photo self._next_id += 1 return new_photo @@ -24,19 +31,18 @@ class PhotoServiceMock(PhotoService): def read_all(self) -> List[Photo]: return list(self.mock_data.values()) - def update(self, photo_id: Photo, photo_dst: Photo) -> Photo | None: - item_to_update:Photo = self.mock_data.get(photo_id) + def update(self, photo_src: Photo, photo_dst: Photo) -> Photo | None: + item_to_update: Photo = self.mock_data.get(photo_src.id) if item_to_update: item_to_update.filepath = photo_dst.filepath if photo_dst.filepath else item_to_update.filepath item_to_update.name = photo_dst.name if photo_dst.name else item_to_update.name item_to_update.caption = photo_dst.caption if photo_dst.caption else item_to_update.caption - item_to_update.addition_date = photo_dst.addition_date if photo_dst.addition_date\ - else item_to_update.addition_date - item_to_update.fk_travel_diary_id = photo_dst.fk_travel_diary_id if photo_dst.fk_travel_diary_id \ - else item_to_update.fk_travel_diary_id - item_to_update.entries.extend(photo_dst.entries) + item_to_update.addition_date = photo_dst.addition_date if photo_dst.addition_date else item_to_update.addition_date + item_to_update.fk_travel_diary_id = photo_dst.fk_travel_diary_id if photo_dst.fk_travel_diary_id else item_to_update.fk_travel_diary_id + if photo_dst.entries: + item_to_update.entries = photo_dst.entries return item_to_update return None - def delete(self, photo_id: int) -> Photo | None: - return self.mock_data.pop(photo_id, None) + def delete(self, photo_src: Photo) -> Photo | None: + return self.mock_data.pop(photo_src.id, None) diff --git a/src/pilgrim/service/photo_service.py b/src/pilgrim/service/photo_service.py index ef7c0c6..df87902 100644 --- a/src/pilgrim/service/photo_service.py +++ b/src/pilgrim/service/photo_service.py @@ -1,5 +1,6 @@ from pathlib import Path from typing import List +from datetime import datetime from pilgrim.models.photo import Photo @@ -9,11 +10,25 @@ class PhotoService: def __init__(self, session): self.session = session - def create(self, filepath:Path, name:str, travel_diary_id, addition_date=None, caption=None, ) -> Photo | None: + def create(self, filepath: Path, name: str, travel_diary_id: int, caption=None, addition_date=None) -> Photo | None: travel_diary = self.session.query(TravelDiary).filter(TravelDiary.id == travel_diary_id).first() if not travel_diary: return None - new_photo = Photo(filepath, name, addition_date=addition_date, caption=caption) + + # Convert addition_date string to datetime if needed + if isinstance(addition_date, str): + try: + addition_date = datetime.strptime(addition_date, "%Y-%m-%d %H:%M:%S") + except ValueError: + addition_date = None + + new_photo = Photo( + filepath=filepath, + name=name, + caption=caption, + fk_travel_diary_id=travel_diary_id, + addition_date=addition_date + ) self.session.add(new_photo) self.session.commit() self.session.refresh(new_photo) @@ -25,24 +40,37 @@ class PhotoService: def read_all(self) -> List[Photo]: return self.session.query(Photo).all() - def update(self,photo_src:Photo,photo_dst:Photo) -> Photo | None: - original:Photo = self.read_by_id(photo_src.id) + def update(self, photo_src: Photo, photo_dst: Photo) -> Photo | None: + original: Photo = self.read_by_id(photo_src.id) if original: original.filepath = photo_dst.filepath original.name = photo_dst.name original.addition_date = photo_dst.addition_date original.caption = photo_dst.caption - original.entries.extend(photo_dst.entries) + if photo_dst.entries and len(photo_dst.entries) > 0: + if original.entries is None: + original.entries = [] + original.entries = photo_dst.entries # Replace instead of extend self.session.commit() self.session.refresh(original) return original return None - def delete(self, photo_src:Photo) -> Photo | None: + def delete(self, photo_src: Photo) -> Photo | None: excluded = self.read_by_id(photo_src.id) if excluded: + # Store photo data before deletion + deleted_photo = Photo( + filepath=excluded.filepath, + name=excluded.name, + addition_date=excluded.addition_date, + caption=excluded.caption, + fk_travel_diary_id=excluded.fk_travel_diary_id, + id=excluded.id + ) + self.session.delete(excluded) self.session.commit() - self.session.refresh(excluded) - return excluded + + return deleted_photo return None diff --git a/src/pilgrim/ui/screens/edit_entry_screen.py b/src/pilgrim/ui/screens/edit_entry_screen.py index 3b91d79..1408ccd 100644 --- a/src/pilgrim/ui/screens/edit_entry_screen.py +++ b/src/pilgrim/ui/screens/edit_entry_screen.py @@ -13,6 +13,8 @@ from pilgrim.models.entry import Entry from pilgrim.models.travel_diary import TravelDiary from pilgrim.models.photo import Photo from pilgrim.ui.screens.modals.add_photo_modal import AddPhotoModal +from pilgrim.ui.screens.modals.edit_photo_modal import EditPhotoModal +from pilgrim.ui.screens.modals.confirm_delete_modal import ConfirmDeleteModal from pilgrim.ui.screens.modals.file_picker_modal import FilePickerModal from pilgrim.ui.screens.rename_entry_modal import RenameEntryModal @@ -275,7 +277,7 @@ class EditEntryScreen(Screen): "[b][green]e[/green][/b]: Edit selected photo\n" "[b][yellow]Tab[/yellow][/b]: Back to editor\n" "[b][yellow]F8[/yellow][/b]: Show/hide sidebar\n" - "[b][yellow]F9[/yellow][/b]: Focus Sidebar/Editor" + "[b][yellow]F9[/yellow][/b]: Switch focus (if needed)" ) self.help_text.update(help_text) @@ -301,10 +303,13 @@ class EditEntryScreen(Screen): if self.sidebar_visible: self.sidebar.display = True self._update_sidebar_content() + # Automatically focus the sidebar when opening + self.sidebar_focused = True + self.photo_list.focus() # Notification when opening the sidebar for the first time if not self._sidebar_opened_once: self.notify( - "Sidebar opened! Context-specific shortcuts are always visible in the sidebar help panel.", + "Sidebar opened and focused! Use the shortcuts shown in the help panel.", severity="info" ) self._sidebar_opened_once = True @@ -337,7 +342,7 @@ class EditEntryScreen(Screen): def action_insert_photo(self): """Insert selected photo into text""" if not self.sidebar_focused or not self.sidebar_visible: - self.notify("Use F9 to focus the sidebar before using this shortcut.", severity="warning") + self.notify("Use F8 to open the sidebar first.", severity="warning") return # Get selected photo @@ -345,11 +350,15 @@ class EditEntryScreen(Screen): self.notify("No photo selected", severity="warning") return + # Adjust index because of 'Ingest Photo' at the top + photo_index = self.photo_list.highlighted - 1 + photos = self._load_photos_for_diary(self.diary_id) - if self.photo_list.highlighted >= len(photos): + if photo_index < 0 or photo_index >= len(photos): + self.notify("No photo selected", severity="warning") return - selected_photo = photos[self.photo_list.highlighted] + selected_photo = photos[photo_index] # Insert photo reference into text photo_ref = f"\n[📷 {selected_photo.name}]({selected_photo.filepath})\n" @@ -367,7 +376,7 @@ class EditEntryScreen(Screen): def action_ingest_new_photo(self): """Ingest a new photo using modal""" if not self.sidebar_focused or not self.sidebar_visible: - self.notify("Use F9 to focus the sidebar before using this shortcut.", severity="warning") + self.notify("Use F8 to open the sidebar first.", severity="warning") return # Open add photo modal @@ -382,8 +391,10 @@ class EditEntryScreen(Screen): self.notify("Add photo cancelled") return - # Schedule async creation - self.call_later(self._async_create_photo, result) + # Photo was already created in the modal, just refresh the sidebar + if self.sidebar_visible: + self._update_sidebar_content() + self.notify(f"Photo '{result['name']}' added successfully!") async def _async_create_photo(self, photo_data: dict): """Creates a new photo asynchronously""" @@ -415,24 +426,46 @@ class EditEntryScreen(Screen): def action_delete_photo(self): """Delete selected photo""" if not self.sidebar_focused or not self.sidebar_visible: - self.notify("Use F9 to focus the sidebar before using this shortcut.", severity="warning") + self.notify("Use F8 to open the sidebar first.", severity="warning") return if self.photo_list.highlighted is None: self.notify("No photo selected", severity="warning") return + # Adjust index because of 'Ingest Photo' at the top + photo_index = self.photo_list.highlighted - 1 + photos = self._load_photos_for_diary(self.diary_id) - if self.photo_list.highlighted >= len(photos): + if photo_index < 0 or photo_index >= len(photos): + self.notify("No photo selected", severity="warning") return - selected_photo = photos[self.photo_list.highlighted] + selected_photo = photos[photo_index] - # Confirm deletion - self.notify(f"Deleting photo: {selected_photo.name}") - - # Schedule async deletion - self.call_later(self._async_delete_photo, selected_photo) + # Open confirm delete modal + self.app.push_screen( + ConfirmDeleteModal(photo=selected_photo), + self.handle_delete_photo_result + ) + + def handle_delete_photo_result(self, result: bool) -> None: + """Callback that processes the delete photo modal result.""" + if result: + # Get the selected photo with adjusted index + photos = self._load_photos_for_diary(self.diary_id) + photo_index = self.photo_list.highlighted - 1 # Adjust for 'Ingest Photo' at top + + if self.photo_list.highlighted is None or photo_index < 0 or photo_index >= len(photos): + self.notify("Photo no longer available", severity="error") + return + + selected_photo = photos[photo_index] + + # Schedule async deletion + self.call_later(self._async_delete_photo, selected_photo) + else: + self.notify("Delete cancelled") async def _async_delete_photo(self, photo: Photo): """Deletes a photo asynchronously""" @@ -456,18 +489,22 @@ class EditEntryScreen(Screen): def action_edit_photo(self): """Edit selected photo using modal""" if not self.sidebar_focused or not self.sidebar_visible: - self.notify("Use F9 to focus the sidebar before using this shortcut.", severity="warning") + self.notify("Use F8 to open the sidebar first.", severity="warning") return if self.photo_list.highlighted is None: self.notify("No photo selected", severity="warning") return + # Adjust index because of 'Ingest Photo' at the top + photo_index = self.photo_list.highlighted - 1 + photos = self._load_photos_for_diary(self.diary_id) - if self.photo_list.highlighted >= len(photos): + if photo_index < 0 or photo_index >= len(photos): + self.notify("No photo selected", severity="warning") return - selected_photo = photos[self.photo_list.highlighted] + selected_photo = photos[photo_index] # Open edit photo modal self.app.push_screen( @@ -481,13 +518,15 @@ class EditEntryScreen(Screen): self.notify("Edit photo cancelled") return - # Get the selected photo + # Get the selected photo with adjusted index photos = self._load_photos_for_diary(self.diary_id) - if self.photo_list.highlighted is None or self.photo_list.highlighted >= len(photos): + photo_index = self.photo_list.highlighted - 1 # Adjust for 'Ingest Photo' at top + + if self.photo_list.highlighted is None or photo_index < 0 or photo_index >= len(photos): self.notify("Photo no longer available", severity="error") return - selected_photo = photos[self.photo_list.highlighted] + selected_photo = photos[photo_index] # Schedule async update self.call_later(self._async_update_photo, selected_photo, result) @@ -504,7 +543,7 @@ class EditEntryScreen(Screen): name=photo_data["name"], addition_date=original_photo.addition_date, caption=photo_data["caption"], - entries=original_photo.entries, + entries=original_photo.entries if original_photo.entries is not None else [], id=original_photo.id ) diff --git a/src/pilgrim/ui/screens/modals/add_photo_modal.py b/src/pilgrim/ui/screens/modals/add_photo_modal.py index 4d5453c..e0ac578 100644 --- a/src/pilgrim/ui/screens/modals/add_photo_modal.py +++ b/src/pilgrim/ui/screens/modals/add_photo_modal.py @@ -1,4 +1,5 @@ import os +from pathlib import Path from textual.app import ComposeResult from textual.screen import Screen from textual.widgets import Static, Input, Button @@ -47,15 +48,58 @@ class AddPhotoModal(Screen): if not filepath.strip() or not name.strip(): self.notify("File path and name are required", severity="error") return - self.result = { + + # Try to create the photo in the database + self.call_later(self._async_create_photo, { "filepath": filepath.strip(), "name": name.strip(), "caption": caption.strip() if caption.strip() else None - } - self.dismiss() + }) elif event.button.id == "cancel-button": self.dismiss() + async def _async_create_photo(self, photo_data: dict): + """Creates a new photo asynchronously using PhotoService""" + try: + service_manager = self.app.service_manager + photo_service = service_manager.get_photo_service() + + new_photo = photo_service.create( + filepath=Path(photo_data["filepath"]), + name=photo_data["name"], + travel_diary_id=self.diary_id, + caption=photo_data["caption"] + ) + + if new_photo: + self.notify(f"Photo '{new_photo.name}' added successfully!") + # Return the created photo data to the calling screen + self.result = { + "filepath": photo_data["filepath"], + "name": photo_data["name"], + "caption": photo_data["caption"], + "photo_id": new_photo.id + } + self.dismiss(self.result) + else: + self.notify("Error creating photo in database", severity="error") + + except Exception as e: + self.notify(f"Error creating photo: {str(e)}", severity="error") + def handle_file_picker_result(self, result: str | None) -> None: if result: - self.query_one("#filepath-input", Input).value = result \ No newline at end of file + # Set the filepath input value + filepath_input = self.query_one("#filepath-input", Input) + filepath_input.value = result + # Trigger the input change event to update the UI + filepath_input.refresh() + # Auto-fill the name field with the filename (without extension) + filename = Path(result).stem + name_input = self.query_one("#name-input", Input) + if not name_input.value.strip(): + name_input.value = filename + name_input.refresh() + else: + # User cancelled the file picker + self.notify("File selection cancelled", severity="information") \ No newline at end of file diff --git a/src/pilgrim/ui/screens/modals/file_picker_modal.py b/src/pilgrim/ui/screens/modals/file_picker_modal.py index 96cd6f7..c65f206 100644 --- a/src/pilgrim/ui/screens/modals/file_picker_modal.py +++ b/src/pilgrim/ui/screens/modals/file_picker_modal.py @@ -25,7 +25,6 @@ class FilePickerModal(Screen): self.start_path = Path(start_path or os.getcwd()) # Start one level up to make navigation easier self.current_path = self.start_path.parent - self.result = None def compose(self) -> ComposeResult: yield Container( @@ -45,8 +44,8 @@ class FilePickerModal(Screen): # Check if it's an image file image_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'} if file_path.suffix.lower() in image_extensions: - self.result = str(file_path) - self.dismiss() + # Return the file path as result + self.dismiss(str(file_path)) else: self.notify("Please select an image file", severity="warning") @@ -63,4 +62,5 @@ class FilePickerModal(Screen): tree.path = str(self.current_path) tree.reload() elif event.button.id == "cancel-button": - self.dismiss() \ No newline at end of file + # Return None to indicate cancellation + 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 a485291..5b52192 100644 --- a/src/pilgrim/ui/styles/pilgrim.css +++ b/src/pilgrim/ui/styles/pilgrim.css @@ -588,4 +588,42 @@ Screen.-modal { height: 1fr; border: solid $accent; margin: 1; +} + +/* ConfirmDeleteModal styles */ +.ConfirmDeleteModal-Dialog { + layout: vertical; + width: 60%; + height: auto; + background: $surface; + border: thick $error; + padding: 2 4; + align: center middle; +} +.ConfirmDeleteModal-Title { + text-align: center; + text-style: bold; + color: $error; + margin-bottom: 1; +} +.ConfirmDeleteModal-Message { + text-align: center; + color: $text; + margin-bottom: 1; +} +.ConfirmDeleteModal-Warning { + text-align: center; + color: $warning; + text-style: italic; + margin-bottom: 2; +} +.ConfirmDeleteModal-Buttons { + width: 1fr; + height: auto; + align: center middle; + padding-top: 1; +} +.ConfirmDeleteModal-Button { + margin: 0 1; + width: 1fr; } \ No newline at end of file From 1209b8147c4b6a5fda04338b6deabc7a862231f1 Mon Sep 17 00:00:00 2001 From: Gustavo Henrique Santos Souza de Miranda Date: Sun, 29 Jun 2025 21:50:46 -0300 Subject: [PATCH 2/3] Added a confirm_delete_modal.py to avoid accidental delete of the photo. --- .../ui/screens/modals/confirm_delete_modal.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/pilgrim/ui/screens/modals/confirm_delete_modal.py diff --git a/src/pilgrim/ui/screens/modals/confirm_delete_modal.py b/src/pilgrim/ui/screens/modals/confirm_delete_modal.py new file mode 100644 index 0000000..b987de4 --- /dev/null +++ b/src/pilgrim/ui/screens/modals/confirm_delete_modal.py @@ -0,0 +1,32 @@ +from textual.app import ComposeResult +from textual.screen import Screen +from textual.widgets import Static, Button +from textual.containers import Container, Horizontal +from pilgrim.models.photo import Photo + +class ConfirmDeleteModal(Screen): + """Modal for confirming photo deletion""" + def __init__(self, photo: Photo): + super().__init__() + self.photo = photo + self.result = None + + def compose(self) -> ComposeResult: + yield Container( + Static("🗑️ Confirm Deletion", classes="ConfirmDeleteModal-Title"), + Static(f"Are you sure you want to delete the photo '{self.photo.name}'?", classes="ConfirmDeleteModal-Message"), + Static("This action cannot be undone.", classes="ConfirmDeleteModal-Warning"), + Horizontal( + Button("Delete", variant="error", id="delete-button", classes="ConfirmDeleteModal-Button"), + Button("Cancel", variant="default", id="cancel-button", classes="ConfirmDeleteModal-Button"), + classes="ConfirmDeleteModal-Buttons" + ), + classes="ConfirmDeleteModal-Dialog" + ) + + def on_button_pressed(self, event: Button.Pressed) -> None: + if event.button.id == "delete-button": + self.result = True + self.dismiss(True) + elif event.button.id == "cancel-button": + self.dismiss(False) \ No newline at end of file From cf56fda3dea20204333ffb4b984603bf0e353294 Mon Sep 17 00:00:00 2001 From: Gustavo Henrique Santos Souza de Miranda Date: Sun, 29 Jun 2025 21:51:27 -0300 Subject: [PATCH 3/3] Added an edit_photo_modal.py to allow modification of the photo after ingestion --- .../ui/screens/modals/edit_photo_modal.py | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 src/pilgrim/ui/screens/modals/edit_photo_modal.py diff --git a/src/pilgrim/ui/screens/modals/edit_photo_modal.py b/src/pilgrim/ui/screens/modals/edit_photo_modal.py new file mode 100644 index 0000000..5af3011 --- /dev/null +++ b/src/pilgrim/ui/screens/modals/edit_photo_modal.py @@ -0,0 +1,68 @@ +from textual.app import ComposeResult +from textual.screen import Screen +from textual.widgets import Static, Input, Button +from textual.containers import Container, Horizontal +from pilgrim.models.photo import Photo + +class EditPhotoModal(Screen): + """Modal for editing an existing photo (name and caption only)""" + def __init__(self, photo: Photo): + super().__init__() + self.photo = photo + self.result = None + + def compose(self) -> ComposeResult: + yield Container( + Static("✏️ Edit Photo", classes="EditPhotoModal-Title"), + Static("File path (read-only):", classes="EditPhotoModal-Label"), + Input( + value=self.photo.filepath, + id="filepath-input", + classes="EditPhotoModal-Input", + disabled=True + ), + Static("Photo name:", classes="EditPhotoModal-Label"), + Input( + value=self.photo.name, + placeholder="Enter photo name...", + id="name-input", + classes="EditPhotoModal-Input" + ), + Static("Caption (optional):", classes="EditPhotoModal-Label"), + Input( + value=self.photo.caption or "", + placeholder="Enter caption...", + id="caption-input", + classes="EditPhotoModal-Input" + ), + Horizontal( + Button("Save Changes", id="save-button", classes="EditPhotoModal-Button"), + Button("Cancel", id="cancel-button", classes="EditPhotoModal-Button"), + classes="EditPhotoModal-Buttons" + ), + classes="EditPhotoModal-Dialog" + ) + + def on_button_pressed(self, event: Button.Pressed) -> None: + if event.button.id == "save-button": + name = self.query_one("#name-input", Input).value + caption = self.query_one("#caption-input", Input).value + + if not name.strip(): + self.notify("Photo name is required", severity="error") + return + + # Return the updated photo data + self.result = { + "filepath": self.photo.filepath, # Keep original filepath + "name": name.strip(), + "caption": caption.strip() if caption.strip() else None + } + self.dismiss(self.result) + + elif event.button.id == "cancel-button": + self.dismiss() + + def on_mount(self) -> None: + """Focus on the name input when modal opens""" + self.query_one("#name-input", Input).focus() \ No newline at end of file