mirror of https://github.com/gmbrax/Pilgrim.git
				
				
				
			Merge pull request #12 from gmbrax/feat/photo-sidebar-tui
Feat/photo sidebar tui
This commit is contained in:
		
						commit
						43919cabb9
					
				|  | @ -1,6 +1,8 @@ | |||
| # Database files | ||||
| database.db | ||||
| 
 | ||||
| .build-vend/ | ||||
| dist_nuitka/ | ||||
| # Byte-compiled / optimized / DLL files | ||||
| __pycache__/ | ||||
| *.py[cod] | ||||
|  | @ -141,4 +143,4 @@ cython_debug/ | |||
| 
 | ||||
| # IDE settings | ||||
| .vscode/ | ||||
| .idea/ | ||||
| .idea/ | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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() | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|             ) | ||||
| 
 | ||||
|  |  | |||
|  | @ -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  | ||||
|             # 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")  | ||||
|  | @ -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)  | ||||
|  | @ -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()  | ||||
|  | @ -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()  | ||||
|             # Return None to indicate cancellation | ||||
|             self.dismiss(None)  | ||||
|  | @ -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; | ||||
| } | ||||
		Loading…
	
		Reference in New Issue