mirror of https://github.com/gmbrax/Pilgrim.git
				
				
				
			Merge pull request #17 from gmbrax/feat/photo-reference-system
Feat/photo reference system
This commit is contained in:
		
						commit
						eb511ad756
					
				|  | @ -16,6 +16,7 @@ class Photo(Base): | |||
|     name = Column(String) | ||||
|     addition_date = Column(DateTime, default=datetime.now) | ||||
|     caption = Column(String) | ||||
|     photo_hash = Column(String,name='hash') | ||||
|     entries = relationship( | ||||
|         "Entry", | ||||
|         secondary=photo_entry_association, | ||||
|  | @ -24,7 +25,7 @@ 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, fk_travel_diary_id=None, **kw: Any): | ||||
|     def __init__(self, filepath, name, photo_hash, addition_date=None, caption=None, entries=None, fk_travel_diary_id=None, **kw: Any): | ||||
|         super().__init__(**kw) | ||||
|         # Convert Path to string if needed | ||||
|         if isinstance(filepath, Path): | ||||
|  | @ -34,6 +35,7 @@ class Photo(Base): | |||
|         self.name = name | ||||
|         self.addition_date = addition_date if addition_date is not None else datetime.now() | ||||
|         self.caption = caption | ||||
|         self.photo_hash = photo_hash | ||||
|         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 | ||||
|  |  | |||
|  | @ -3,32 +3,39 @@ from typing import List | |||
| 
 | ||||
| from ..models.entry import Entry | ||||
| from ..models.travel_diary import TravelDiary | ||||
| from ..models.photo import Photo  # ✨ Importe o modelo Photo | ||||
| 
 | ||||
| 
 | ||||
| class EntryService: | ||||
|     def __init__(self,session): | ||||
|     def __init__(self, session): | ||||
|         self.session = session | ||||
| 
 | ||||
|     def create(self, travel_diary_id:int, title: str, text: str, date: datetime, ): | ||||
|     # ✨ Modifique a assinatura para aceitar a lista de fotos | ||||
|     def create(self, travel_diary_id: int, title: str, text: str, date: datetime, photos: List[Photo]): | ||||
|         travel_diary = self.session.query(TravelDiary).filter(TravelDiary.id == travel_diary_id).first() | ||||
|         if not travel_diary: | ||||
|             return None | ||||
|         new_entry = Entry(title,text,date,travel_diary_id) | ||||
| 
 | ||||
|         new_entry = Entry(title, text, date, travel_diary_id,photos=photos) | ||||
| 
 | ||||
|         # ✨ Atribua a relação ANTES de adicionar e fazer o commit | ||||
|         new_entry.photos = photos | ||||
| 
 | ||||
|         self.session.add(new_entry) | ||||
|         self.session.commit() | ||||
|         self.session.refresh(new_entry) | ||||
|         return new_entry | ||||
| 
 | ||||
|     def read_by_id(self,entry_id:int)->Entry: | ||||
|     def read_by_id(self, entry_id: int) -> Entry: | ||||
|         entry = self.session.query(Entry).filter(Entry.id == entry_id).first() | ||||
|         return entry | ||||
| 
 | ||||
|     def read_all(self)-> List[Entry]: | ||||
|     def read_all(self) -> List[Entry]: | ||||
|         entries = self.session.query(Entry).all() | ||||
|         return entries | ||||
| 
 | ||||
|     def update(self,entry_src:Entry,entry_dst:Entry) -> Entry | None: | ||||
|         original:Entry = self.read_by_id(entry_src.id) | ||||
|     def update(self, entry_src: Entry, entry_dst: Entry) -> Entry | None: | ||||
|         original: Entry = self.read_by_id(entry_src.id) | ||||
|         if original: | ||||
|             original.title = entry_dst.title | ||||
|             original.text = entry_dst.text | ||||
|  | @ -40,7 +47,7 @@ class EntryService: | |||
|             return original | ||||
|         return None | ||||
| 
 | ||||
|     def delete(self,entry_src:Entry)-> Entry | None: | ||||
|     def delete(self, entry_src: Entry) -> Entry | None: | ||||
|         excluded = self.read_by_id(entry_src.id) | ||||
|         if excluded is not None: | ||||
|             self.session.delete(excluded) | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| from pathlib import Path | ||||
| from typing import List | ||||
| from datetime import datetime | ||||
| import hashlib | ||||
| 
 | ||||
| 
 | ||||
| from pilgrim.models.photo import Photo | ||||
|  | @ -9,6 +10,12 @@ from pilgrim.models.travel_diary import TravelDiary | |||
| class PhotoService: | ||||
|     def __init__(self, session): | ||||
|         self.session = session | ||||
|     def _hash_file(self,filepath): | ||||
|         hash_func = hashlib.new('sha3_384') | ||||
|         with open(filepath, 'rb') as f: | ||||
|             while chunk := f.read(8192): | ||||
|                 hash_func.update(chunk) | ||||
|         return hash_func.hexdigest() | ||||
| 
 | ||||
|     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() | ||||
|  | @ -27,7 +34,8 @@ class PhotoService: | |||
|             name=name,  | ||||
|             caption=caption,  | ||||
|             fk_travel_diary_id=travel_diary_id, | ||||
|             addition_date=addition_date | ||||
|             addition_date=addition_date, | ||||
|             photo_hash=self._hash_file(filepath) | ||||
|         ) | ||||
|         self.session.add(new_photo) | ||||
|         self.session.commit() | ||||
|  | @ -47,6 +55,7 @@ class PhotoService: | |||
|             original.name = photo_dst.name | ||||
|             original.addition_date = photo_dst.addition_date | ||||
|             original.caption = photo_dst.caption | ||||
|             original.photo_hash = original.photo_hash | ||||
|             if photo_dst.entries and len(photo_dst.entries) > 0: | ||||
|                 if original.entries is None: | ||||
|                     original.entries = [] | ||||
|  | @ -66,7 +75,10 @@ class PhotoService: | |||
|                 addition_date=excluded.addition_date, | ||||
|                 caption=excluded.caption, | ||||
|                 fk_travel_diary_id=excluded.fk_travel_diary_id, | ||||
|                 id=excluded.id | ||||
|                 id=excluded.id, | ||||
|                 photo_hash=excluded.photo_hash, | ||||
|                 entries=excluded.entries, | ||||
| 
 | ||||
|             ) | ||||
|              | ||||
|             self.session.delete(excluded) | ||||
|  |  | |||
|  | @ -2,6 +2,9 @@ from typing import Optional, List | |||
| import asyncio | ||||
| from datetime import datetime | ||||
| from pathlib import Path | ||||
| import hashlib | ||||
| import re | ||||
| import time | ||||
| 
 | ||||
| from textual.app import ComposeResult | ||||
| from textual.screen import Screen | ||||
|  | @ -23,13 +26,15 @@ 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"), | ||||
|         Binding("f8", "toggle_sidebar", "Toggle Sidebar"), | ||||
|         Binding("f9", "toggle_focus", "Focus Sidebar/Editor"), | ||||
|         ("ctrl+q", "quit", "Quit"), | ||||
|         ("ctrl+s", "save", "Save"), | ||||
|         ("ctrl+n", "new_entry", "New Entry"), | ||||
|         ("ctrl+shift+n", "next_entry", "Next Entry"), | ||||
|         ("ctrl+shift+p", "prev_entry", "Previous Entry"), | ||||
|         ("ctrl+r", "rename_entry", "Rename Entry"), | ||||
|         ("f8", "toggle_sidebar", "Toggle Photos"), | ||||
|         ("f9", "toggle_focus", "Toggle Focus"), | ||||
|         ("escape", "back_to_list", "Back to List"), | ||||
|     ] | ||||
| 
 | ||||
|     def __init__(self, diary_id: int = 1): | ||||
|  | @ -50,6 +55,13 @@ class EditEntryScreen(Screen): | |||
|         self.sidebar_visible = False | ||||
|         self.sidebar_focused = False | ||||
|         self._sidebar_opened_once = False | ||||
|         self._active_tooltip = None | ||||
|         self._last_photo_suggestion_notification = None | ||||
|         self._last_photo_suggestion_type = None | ||||
|         self._active_notification = None | ||||
|         self._notification_timer = None | ||||
|         self.references = [] | ||||
|         self.cached_photos = [] | ||||
| 
 | ||||
|         # Main header | ||||
|         self.header = Header(name="Pilgrim v6", classes="EditEntryScreen-header") | ||||
|  | @ -105,9 +117,28 @@ class EditEntryScreen(Screen): | |||
|         self.footer = Footer(classes="EditEntryScreen-footer") | ||||
| 
 | ||||
|     def _update_footer_context(self): | ||||
|         """Forces footer refresh to show updated bindings""" | ||||
|         """Force footer refresh to show updated bindings""" | ||||
|         self.refresh() | ||||
| 
 | ||||
|     def _get_cursor_position(self) -> tuple: | ||||
|         """Get the current cursor position for tooltip placement""" | ||||
|         try: | ||||
|             # Get cursor position from text area | ||||
|             cursor_location = self.text_entry.cursor_location | ||||
|             if cursor_location: | ||||
|                 # Get the text area region | ||||
|                 text_region = self.text_entry.region | ||||
|                 if text_region: | ||||
|                     # Calculate position relative to text area | ||||
|                     # Position tooltip below the current line, not over it | ||||
|                     x = text_region.x + min(cursor_location[0], text_region.width - 40)  # Keep within bounds | ||||
|                     y = text_region.y + cursor_location[1] + 2  # 2 lines below cursor | ||||
|                     return (x, y) | ||||
|         except: | ||||
|             pass | ||||
|         return None | ||||
| 
 | ||||
| 
 | ||||
|     def compose(self) -> ComposeResult: | ||||
|         print("DEBUG: EditEntryScreen COMPOSE", getattr(self, 'sidebar_visible', None)) | ||||
|         yield self.header | ||||
|  | @ -123,20 +154,21 @@ class EditEntryScreen(Screen): | |||
|         """Called when the screen is mounted""" | ||||
|         self.sidebar.display = False | ||||
|         self.sidebar_visible = False | ||||
|          | ||||
| 
 | ||||
|         # First update diary info, then refresh entries | ||||
|         self.update_diary_info() | ||||
|         self.refresh_entries() | ||||
|          | ||||
| 
 | ||||
|         # Initialize footer with editor context | ||||
|         self._update_footer_context() | ||||
|         # self.app.mount(self._photo_suggestion_widget)  # Temporarily disabled | ||||
| 
 | ||||
|     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 | ||||
|  | @ -149,7 +181,7 @@ class EditEntryScreen(Screen): | |||
|             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)}") | ||||
|          | ||||
| 
 | ||||
|         self._ensure_diary_info_updated() | ||||
| 
 | ||||
|     def _ensure_diary_info_updated(self): | ||||
|  | @ -179,7 +211,7 @@ class EditEntryScreen(Screen): | |||
| 
 | ||||
|         except Exception as e: | ||||
|             self.notify(f"Error loading entries: {str(e)}") | ||||
|          | ||||
| 
 | ||||
|         self._ensure_diary_info_updated() | ||||
| 
 | ||||
|     def _update_status_indicator(self, text: str, css_class: str): | ||||
|  | @ -249,141 +281,184 @@ class EditEntryScreen(Screen): | |||
| 
 | ||||
|     def _update_sidebar_content(self): | ||||
|         """Updates the sidebar content with photos for the current diary""" | ||||
|         photos = self._load_photos_for_diary(self.diary_id) | ||||
|         try: | ||||
|             self._load_photos_for_diary(self.diary_id) | ||||
| 
 | ||||
|         # Clear existing options safely | ||||
|         self.photo_list.clear_options() | ||||
|             # Clear existing options safely | ||||
|             self.photo_list.clear_options() | ||||
| 
 | ||||
|         # Add 'Ingest Photo' option at the top | ||||
|         self.photo_list.add_option("➕ Ingest Photo") | ||||
|             # Add the 'Ingest Photo' option at the top | ||||
|             self.photo_list.add_option("➕ Ingest Photo") | ||||
| 
 | ||||
|         if not photos: | ||||
|             self.photo_info.update("No photos found for this diary") | ||||
|             self.help_text.update("📸 No photos available\n\nUse Photo Manager to add photos") | ||||
|             return | ||||
|             if not self.cached_photos: | ||||
|                 self.photo_info.update("No photos found for this diary") | ||||
|                 self.help_text.update("📸 No photos available\n\nUse Photo Manager to add photos") | ||||
|                 return | ||||
| 
 | ||||
|         # Add photos to the list | ||||
|         for photo in photos: | ||||
|             self.photo_list.add_option(f"📷 {photo.name}") | ||||
|             # Add photos to the list with hash | ||||
|             for photo in self.cached_photos: | ||||
|                 # Show name and hash in the list | ||||
|                 photo_hash = str(photo.photo_hash)[:8] | ||||
|                 self.photo_list.add_option(f"📷 {photo.name} \\[{photo_hash}\]") | ||||
| 
 | ||||
|         self.photo_info.update(f"📸 {len(photos)} photos in diary") | ||||
|          | ||||
|         # English, visually distinct help text | ||||
|         help_text = ( | ||||
|             "[b]⌨️  Sidebar Shortcuts[/b]\n" | ||||
|             "[b][green]i[/green][/b]: Insert photo into entry\n" | ||||
|             "[b][green]n[/green][/b]: Add new photo\n" | ||||
|             "[b][green]d[/green][/b]: Delete selected photo\n" | ||||
|             "[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]: Switch focus (if needed)" | ||||
|         ) | ||||
|         self.help_text.update(help_text) | ||||
|             self.photo_info.update(f"📸 {len(self.cached_photos)} photos in diary") | ||||
| 
 | ||||
|     def _load_photos_for_diary(self, diary_id: int) -> List[Photo]: | ||||
|             # Updated help a text with hash information | ||||
|             help_text = ( | ||||
|                 "[b]⌨️  Sidebar Shortcuts[/b]\n" | ||||
|                 "[b][green]i[/green][/b]: Insert photo into entry\n" | ||||
|                 "[b][green]n[/green][/b]: Add new photo\n" | ||||
|                 "[b][green]d[/green][/b]: Delete selected photo\n" | ||||
|                 "[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]: Switch focus (if needed)\n\n" | ||||
|                 "[b]📝 Photo References[/b]\n" | ||||
|                 "Use: \\[\\[photo:name:hash\\]\\]\n" | ||||
|                 "Or: \\[\\[photo::hash\\]\\]" | ||||
|             ) | ||||
|             self.help_text.update(help_text) | ||||
|         except Exception as e: | ||||
|             self.notify(f"Error updating sidebar: {str(e)}", severity="error") | ||||
|             # Set fallback content | ||||
|             self.photo_info.update("Error loading photos") | ||||
|             self.help_text.update("Error loading sidebar content") | ||||
| 
 | ||||
|     def _load_photos_for_diary(self, diary_id: int): | ||||
|         """Loads all photos for the specific diary""" | ||||
|         try: | ||||
|             service_manager = self.app.service_manager | ||||
|             photo_service = service_manager.get_photo_service() | ||||
|              | ||||
| 
 | ||||
|             all_photos = photo_service.read_all() | ||||
|             photos = [photo for photo in all_photos if photo.fk_travel_diary_id == diary_id] | ||||
|             photos.sort(key=lambda x: x.id) | ||||
|             return photos | ||||
|             self.cached_photos = [photo for photo in all_photos if photo.fk_travel_diary_id == diary_id] | ||||
|             self.cached_photos.sort(key=lambda x: x.id) | ||||
| 
 | ||||
|         except Exception as e: | ||||
|             self.notify(f"Error loading photos: {str(e)}") | ||||
|             return [] | ||||
| 
 | ||||
| 
 | ||||
|     def action_toggle_sidebar(self): | ||||
|         """Toggles the sidebar visibility""" | ||||
|         print("DEBUG: TOGGLE SIDEBAR", self.sidebar_visible) | ||||
|         self.sidebar_visible = not self.sidebar_visible | ||||
|          | ||||
|         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 and focused! Use the shortcuts shown in the help panel.", | ||||
|                     severity="info" | ||||
|                 ) | ||||
|                 self._sidebar_opened_once = True | ||||
|         else: | ||||
|         try: | ||||
|             print("DEBUG: TOGGLE SIDEBAR", self.sidebar_visible) | ||||
|             self.sidebar_visible = not self.sidebar_visible | ||||
| 
 | ||||
|             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 and focused! Use the shortcuts shown in the help panel.", | ||||
|                         severity="info" | ||||
|                     ) | ||||
|                     self._sidebar_opened_once = True | ||||
|             else: | ||||
|                 self.sidebar.display = False | ||||
|                 self.sidebar_focused = False  # Reset focus when hiding | ||||
|                 self.text_entry.focus()  # Return focus to editor | ||||
| 
 | ||||
|             # Update footer after context change | ||||
|             self._update_footer_context() | ||||
|             self.refresh(layout=True) | ||||
|         except Exception as e: | ||||
|             self.notify(f"Error toggling sidebar: {str(e)}", severity="error") | ||||
|             # Reset state on error | ||||
|             self.sidebar_visible = False | ||||
|             self.sidebar_focused = False | ||||
|             self.sidebar.display = False | ||||
|             self.sidebar_focused = False  # Reset focus when hiding | ||||
|             self.text_entry.focus()  # Return focus to editor | ||||
|          | ||||
|         # Update footer after context change | ||||
|         self._update_footer_context() | ||||
|         self.refresh(layout=True) | ||||
| 
 | ||||
|     def action_toggle_focus(self): | ||||
|         """Toggles focus between editor and sidebar""" | ||||
|         print("DEBUG: TOGGLE FOCUS", self.sidebar_visible, self.sidebar_focused) | ||||
|         print("DEBUG: TOGGLE FOCUS called", self.sidebar_visible, self.sidebar_focused) | ||||
|         if not self.sidebar_visible: | ||||
|             # If sidebar is not visible, show it and focus it | ||||
|             print("DEBUG: Sidebar not visible, opening it") | ||||
|             self.action_toggle_sidebar() | ||||
|             return | ||||
|          | ||||
| 
 | ||||
|         self.sidebar_focused = not self.sidebar_focused | ||||
|         print("DEBUG: Sidebar focused changed to", self.sidebar_focused) | ||||
|         if self.sidebar_focused: | ||||
|             self.photo_list.focus() | ||||
|         else: | ||||
|             self.text_entry.focus() | ||||
|          | ||||
| 
 | ||||
|         # Update footer after focus change | ||||
|         self._update_footer_context() | ||||
| 
 | ||||
|     def action_insert_photo(self): | ||||
|         """Insert selected photo into text""" | ||||
| 
 | ||||
|         if not self.sidebar_focused or not self.sidebar_visible: | ||||
|             self.notify("Use F8 to open the sidebar first.", severity="warning") | ||||
|             return | ||||
|              | ||||
|         # Get selected photo | ||||
| 
 | ||||
|         # Get a selected photo | ||||
|         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 photo_index < 0 or photo_index >= len(photos): | ||||
| 
 | ||||
|         self._load_photos_for_diary(self.diary_id) | ||||
|         if photo_index < 0 or photo_index >= len(self.cached_photos): | ||||
|             self.notify("No photo selected", severity="warning") | ||||
|             return | ||||
|              | ||||
|         selected_photo = photos[photo_index] | ||||
|          | ||||
|         # Insert photo reference into text | ||||
|         photo_ref = f"\n[📷 {selected_photo.name}]({selected_photo.filepath})\n" | ||||
|         if selected_photo.caption: | ||||
|             photo_ref += f"*{selected_photo.caption}*\n" | ||||
|          | ||||
|         # Insert at cursor position or at end | ||||
|         current_text = self.text_entry.text | ||||
|         cursor_position = len(current_text)  # Insert at end for now | ||||
|         new_text = current_text + photo_ref | ||||
|         self.text_entry.text = new_text | ||||
|          | ||||
|         self.notify(f"Inserted photo: {selected_photo.name}") | ||||
| 
 | ||||
|         selected_photo = self.cached_photos[photo_index] | ||||
|         photo_hash = selected_photo.photo_hash[:8] | ||||
| 
 | ||||
|         # Insert photo reference using hash format without escaping | ||||
|         # Using raw string to avoid markup conflicts with [[ | ||||
|         photo_ref = f"[[photo::{photo_hash}]]" | ||||
| 
 | ||||
|         # Insert at the cursor position | ||||
|         self.text_entry.insert(photo_ref) | ||||
| 
 | ||||
|         # Switch focus back to editor | ||||
|         self.sidebar_focused = False | ||||
|         self.text_entry.focus() | ||||
| 
 | ||||
|         # Update footer context | ||||
|         self._update_footer_context() | ||||
| 
 | ||||
|         # Show selected photo info | ||||
|         photo_details = f"📷 {selected_photo.name}\n" | ||||
|         photo_details += f"🔗 {photo_hash}\n" | ||||
|         photo_details += f"📅 {selected_photo.addition_date}\n" | ||||
|         photo_details += f"💬 {selected_photo.caption or 'No caption'}\n" | ||||
|         photo_details += f"📁 {selected_photo.filepath}\n\n" | ||||
|         photo_details += f"[b]Reference formats:[/b]\n" | ||||
|         photo_details += f"\\[\\[photo:{selected_photo.name}:{photo_hash}\\]\\]\n" | ||||
|         photo_details += f"\\[\\[photo::{photo_hash}\\]\\]" | ||||
| 
 | ||||
|         self.photo_info.update(photo_details) | ||||
| 
 | ||||
|         # Show notification without escaping brackets | ||||
|         self.notify(f"Inserted photo: {selected_photo.name} \\[{photo_hash}\\]", severity="information") | ||||
| 
 | ||||
|     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 F8 to open the sidebar first.", severity="warning") | ||||
|             return | ||||
|          | ||||
| 
 | ||||
|         # Open add photo modal | ||||
|         self.app.push_screen( | ||||
|             AddPhotoModal(diary_id=self.diary_id), | ||||
|             self.handle_add_photo_result | ||||
|         ) | ||||
|         try: | ||||
|             self.notify("Trying to push the modal screen...") | ||||
|             self.app.push_screen( | ||||
|                 AddPhotoModal(diary_id=self.diary_id), | ||||
|                 self.handle_add_photo_result | ||||
|             ) | ||||
|         except Exception as e: | ||||
|             self.notify(f"Error: {str(e)}", severity="error") | ||||
|             self.app.notify("Error: {str(e)}", severity="error") | ||||
| 
 | ||||
|     def handle_add_photo_result(self, result: dict | None) -> None: | ||||
|         """Callback that processes the add photo modal result.""" | ||||
|  | @ -403,7 +478,7 @@ class EditEntryScreen(Screen): | |||
|             photo_service = service_manager.get_photo_service() | ||||
| 
 | ||||
|             current_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S") | ||||
|              | ||||
| 
 | ||||
|             new_photo = photo_service.create( | ||||
|                 filepath=Path(photo_data["filepath"]), | ||||
|                 name=photo_data["name"], | ||||
|  | @ -428,21 +503,21 @@ class EditEntryScreen(Screen): | |||
|         if not self.sidebar_focused or not self.sidebar_visible: | ||||
|             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 photo_index < 0 or photo_index >= len(photos): | ||||
|             self.notify("No photo selected", severity="warning") | ||||
|             return | ||||
|              | ||||
| 
 | ||||
|         selected_photo = photos[photo_index] | ||||
|          | ||||
| 
 | ||||
|         # Open confirm delete modal | ||||
|         self.app.push_screen( | ||||
|             ConfirmDeleteModal(photo=selected_photo), | ||||
|  | @ -452,16 +527,16 @@ class EditEntryScreen(Screen): | |||
|     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 | ||||
|             # Get the selected photo with an 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: | ||||
|  | @ -491,21 +566,21 @@ class EditEntryScreen(Screen): | |||
|         if not self.sidebar_focused or not self.sidebar_visible: | ||||
|             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 photo_index < 0 or photo_index >= len(photos): | ||||
|             self.notify("No photo selected", severity="warning") | ||||
|             return | ||||
|              | ||||
| 
 | ||||
|         selected_photo = photos[photo_index] | ||||
|          | ||||
| 
 | ||||
|         # Open edit photo modal | ||||
|         self.app.push_screen( | ||||
|             EditPhotoModal(photo=selected_photo), | ||||
|  | @ -521,13 +596,13 @@ class EditEntryScreen(Screen): | |||
|         # 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 update | ||||
|         self.call_later(self._async_update_photo, selected_photo, result) | ||||
| 
 | ||||
|  | @ -560,34 +635,140 @@ class EditEntryScreen(Screen): | |||
|         except Exception as e: | ||||
|             self.notify(f"Error updating photo: {str(e)}") | ||||
| 
 | ||||
|     def _get_linked_photos_from_text(self) -> Optional[List[Photo]]: | ||||
|         """ | ||||
|         Validates photo references in the text against the memory cache. | ||||
|         Checks for: | ||||
|         - Malformed references | ||||
|         - Incorrect hash length | ||||
|         - Invalid or ambiguous hashes | ||||
|         Returns a list of unique photos (no duplicates even if referenced multiple times). | ||||
|         """ | ||||
|         text = self.text_entry.text | ||||
|          | ||||
|         # First check for malformed references | ||||
|         malformed_pattern = r"\[\[photo::([^\]]*)\](?!\])"  # Missing ] at the end | ||||
|         malformed_matches = re.findall(malformed_pattern, text) | ||||
|         if malformed_matches: | ||||
|             for match in malformed_matches: | ||||
|                 self.notify(f"❌ Malformed reference: '\\[\\[photo::{match}\\]' - Missing closing '\\]'", severity="error", timeout=10) | ||||
|             return None | ||||
| 
 | ||||
|         # Look for incorrect format references | ||||
|         invalid_format = r"\[\[photo:[^:\]]+\]\]"  # [[photo:something]] without :: | ||||
|         invalid_matches = re.findall(invalid_format, text) | ||||
|         if invalid_matches: | ||||
|             for match in invalid_matches: | ||||
|                 escaped_match = match.replace("[", "\\[").replace("]", "\\]") | ||||
|                 self.notify(f"❌ Invalid format: '{escaped_match}' - Use '\\[\\[photo::hash\\]\\]'", severity="error", timeout=10) | ||||
|             return None | ||||
| 
 | ||||
|         # Now look for all references to validate | ||||
|         pattern = r"\[\[photo::([^\]]+)\]\]" | ||||
|         # Use set to get unique references only | ||||
|         all_refs = set(re.findall(pattern, text)) | ||||
|          | ||||
|         if not all_refs: | ||||
|             return []  # No references, valid operation | ||||
| 
 | ||||
|         self._load_photos_for_diary(self.diary_id) | ||||
|         linked_photos: List[Photo] = [] | ||||
|         processed_hashes = set()  # Keep track of processed hashes to avoid duplicates | ||||
| 
 | ||||
|         for ref in all_refs: | ||||
|             # Skip if we already processed this hash | ||||
|             if ref in processed_hashes: | ||||
|                 continue | ||||
| 
 | ||||
|             # Validate hash length | ||||
|             if len(ref) != 8: | ||||
|                 self.notify( | ||||
|                     f"❌ Invalid hash: '{ref}' - Must be exactly 8 characters long", | ||||
|                     severity="error", | ||||
|                     timeout=10 | ||||
|                 ) | ||||
|                 return None | ||||
| 
 | ||||
|             # Validate if contains only valid hexadecimal characters | ||||
|             if not re.match(r"^[0-9A-Fa-f]{8}$", ref): | ||||
|                 self.notify( | ||||
|                     f"❌ Invalid hash: '{ref}' - Use only hexadecimal characters (0-9, A-F)", | ||||
|                     severity="error", | ||||
|                     timeout=10 | ||||
|                 ) | ||||
|                 return None | ||||
| 
 | ||||
|             # Search for photos matching the hash | ||||
|             found_photos = [p for p in self.cached_photos if p.photo_hash.startswith(ref)] | ||||
| 
 | ||||
|             if len(found_photos) == 0: | ||||
|                 self.notify( | ||||
|                     f"❌ Hash not found: '{ref}' - No photo matches this hash", | ||||
|                     severity="error", | ||||
|                     timeout=10 | ||||
|                 ) | ||||
|                 return None | ||||
|             elif len(found_photos) > 1: | ||||
|                 self.notify( | ||||
|                     f"❌ Ambiguous hash: '{ref}' - Matches multiple photos", | ||||
|                     severity="error", | ||||
|                     timeout=10 | ||||
|                 ) | ||||
|                 return None | ||||
|             else: | ||||
|                 linked_photos.append(found_photos[0]) | ||||
|                 processed_hashes.add(ref)  # Mark this hash as processed | ||||
| 
 | ||||
|         # Convert list to set and back to list to ensure uniqueness of photos | ||||
|         return list(set(linked_photos)) | ||||
| 
 | ||||
|     def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None: | ||||
|         """Handles photo selection in the sidebar""" | ||||
|         if not self.sidebar_visible: | ||||
|             return | ||||
|         # If 'Ingest Photo' is selected (always index 0) | ||||
|         if event.option_index == 0: | ||||
| 
 | ||||
|         # Handle "Ingest Photo" option | ||||
|         if event.option_index == 0:  # First option is "Ingest Photo" | ||||
|             self.action_ingest_new_photo() | ||||
|             return | ||||
| 
 | ||||
|         photos = self._load_photos_for_diary(self.diary_id) | ||||
|         if not photos: | ||||
|             return | ||||
| 
 | ||||
|         # Adjust index because of 'Ingest Photo' at the top | ||||
|         photo_index = event.option_index - 1 | ||||
|         if not photos or photo_index >= len(photos): | ||||
|         if photo_index >= len(photos): | ||||
|             return | ||||
| 
 | ||||
|         selected_photo = photos[photo_index] | ||||
|         self.notify(f"Selected photo: {selected_photo.name}") | ||||
|         # Update photo info with details | ||||
|         photo_hash = selected_photo.photo_hash[:8] | ||||
|         self.notify(f"Selected photo: {selected_photo.name} \\[{photo_hash}\\]") | ||||
| 
 | ||||
|         # Update photo info with details including hash | ||||
|         photo_details = f"📷 {selected_photo.name}\n" | ||||
|         photo_details += f"🔗 {photo_hash}\n" | ||||
|         photo_details += f"📅 {selected_photo.addition_date}\n" | ||||
|         if selected_photo.caption: | ||||
|             photo_details += f"💬 {selected_photo.caption}\n" | ||||
|         photo_details += f"📁 {selected_photo.filepath}" | ||||
|         photo_details += f"📁 {selected_photo.filepath}\n\n" | ||||
|         photo_details += f"[b]Reference formats:[/b]\n" | ||||
|         photo_details += f"\\[\\[photo:{selected_photo.name}:{photo_hash}\\]\\]\n" | ||||
|         photo_details += f"\\[\\[photo::{photo_hash}\\]\\]" | ||||
| 
 | ||||
|         self.photo_info.update(photo_details) | ||||
| 
 | ||||
|     def on_text_area_changed(self, event) -> None: | ||||
|         """Detects text changes to mark as unsaved""" | ||||
|         """Detects text changes and shows photo tooltips""" | ||||
|         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 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|             # Check for a photo reference pattern | ||||
|             # self._check_photo_reference(current_content)  # Temporarily disabled | ||||
| 
 | ||||
|             if current_content != self._original_content: | ||||
|                 if not self.has_unsaved_changes: | ||||
|                     self.has_unsaved_changes = True | ||||
|  | @ -599,7 +780,7 @@ class EditEntryScreen(Screen): | |||
| 
 | ||||
|     def on_focus(self, event) -> None: | ||||
|         """Captures focus changes to update footer""" | ||||
|         # Check if focus changed to/from sidebar | ||||
|         # Check if the focus changed to/from sidebar | ||||
|         if hasattr(event.widget, 'id'): | ||||
|             if event.widget.id == "photo_list": | ||||
|                 self.sidebar_focused = True | ||||
|  | @ -713,35 +894,42 @@ class EditEntryScreen(Screen): | |||
|         self._update_sub_header() | ||||
| 
 | ||||
|     def action_save(self) -> None: | ||||
|         """Saves the current entry""" | ||||
|         """Salva a entrada após validar e coletar as fotos referenciadas.""" | ||||
|         photos_to_link = self._get_linked_photos_from_text() | ||||
| 
 | ||||
|         if photos_to_link is None: | ||||
|             self.notify("⚠️ Saving was canceled ", severity="error") | ||||
|             return | ||||
| 
 | ||||
|         content = self.text_entry.text.strip() | ||||
|         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) | ||||
|             # Passe a lista de fotos para o método de criação | ||||
|             self.call_later(self._async_create_entry, content, photos_to_link) | ||||
|         else: | ||||
|             # Schedule async update | ||||
|             self.call_later(self._async_update_entry) | ||||
|             # Passe a lista de fotos para o método de atualização | ||||
|             self.call_later(self._async_update_entry, content, photos_to_link) | ||||
| 
 | ||||
|     async def _async_create_entry(self, content: str): | ||||
|         """Creates a new entry asynchronously""" | ||||
|     async def _async_create_entry(self, content: str, photos_to_link: List[Photo]): | ||||
|         """Cria uma nova entrada e associa as fotos referenciadas.""" | ||||
|         service_manager = self.app.service_manager | ||||
|         db_session = service_manager.get_db_session() | ||||
|         try: | ||||
|             service_manager = self.app.service_manager | ||||
|             entry_service = service_manager.get_entry_service() | ||||
| 
 | ||||
|             current_date = datetime.now() | ||||
| 
 | ||||
|             # O service.create deve criar o objeto em memória, mas NÃO fazer o commit ainda. | ||||
|             new_entry = entry_service.create( | ||||
|                 travel_diary_id=self.diary_id, | ||||
|                 title=self.new_entry_title, | ||||
|                 text=content, | ||||
|                 date=current_date | ||||
|                 date=datetime.now(), | ||||
|                 photos=photos_to_link | ||||
|             ) | ||||
| 
 | ||||
|             if new_entry: | ||||
|                # A partir daqui, é só atualizar a UI como você já fazia | ||||
|                 self.entries.append(new_entry) | ||||
|                 self.entries.sort(key=lambda x: x.id) | ||||
| 
 | ||||
|  | @ -752,66 +940,67 @@ class EditEntryScreen(Screen): | |||
| 
 | ||||
|                 self.is_new_entry = False | ||||
|                 self.has_unsaved_changes = False | ||||
|                 self._original_content = new_entry.text | ||||
|                 self._original_content = new_entry.text  # Pode ser o texto com hashes curtos | ||||
|                 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!") | ||||
|                 self.notify(f"✅ New Entry: '{new_entry.title}' Successfully saved") | ||||
|             else: | ||||
|                 self.notify("Error creating entry") | ||||
|                 self.notify("❌ Error creating the Entry") | ||||
| 
 | ||||
|         except Exception as e: | ||||
|             self.notify(f"Error creating entry: {str(e)}") | ||||
|             self.notify(f"❌ Error creating the entry: {str(e)}") | ||||
| 
 | ||||
|     async def _async_update_entry(self, updated_content: str, photos_to_link: List[Photo]): | ||||
|         """Atualiza uma entrada existente e sua associação de fotos.""" | ||||
|         service_manager = self.app.service_manager | ||||
| 
 | ||||
|     async def _async_update_entry(self): | ||||
|         """Updates the current entry asynchronously""" | ||||
|         try: | ||||
|             if not self.entries: | ||||
|                 self.notify("No entry to update") | ||||
|                 self.notify("No Entry to update") | ||||
|                 return | ||||
| 
 | ||||
|             entry_service = service_manager.get_entry_service() | ||||
|             current_entry = self.entries[self.current_entry_index] | ||||
|             updated_content = self.text_entry.text | ||||
| 
 | ||||
|             updated_entry = Entry( | ||||
|             entry_result : Entry = Entry( | ||||
|                 title=current_entry.title, | ||||
|                 text=updated_content, | ||||
|                 photos=photos_to_link, | ||||
|                 date=current_entry.date, | ||||
|                 travel_diary_id=current_entry.fk_travel_diary_id, | ||||
|                 id=current_entry.id | ||||
|                 travel_diary_id=self.diary_id | ||||
| 
 | ||||
|             ) | ||||
|             entry_service.update(current_entry, entry_result) | ||||
| 
 | ||||
|             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") | ||||
|             # A partir daqui, é só atualizar a UI | ||||
|             self.has_unsaved_changes = False | ||||
|             self._original_content = updated_content  # Pode ser o texto com hashes curtos | ||||
|             self._update_sub_header() | ||||
|             self.notify(f"✅ Entry: '{current_entry.title}' sucesfully saved") | ||||
| 
 | ||||
|         except Exception as e: | ||||
|             self.notify(f"Error updating entry: {str(e)}") | ||||
|            # Desfaz as mudanças em caso de erro | ||||
|             self.notify(f"❌ Error on updating the entry:: {str(e)}") | ||||
| 
 | ||||
|     def on_key(self, event): | ||||
| 
 | ||||
|         # Sidebar contextual shortcuts | ||||
|         if self.sidebar_focused and self.sidebar_visible: | ||||
| 
 | ||||
|             if event.key == "i": | ||||
| 
 | ||||
|                 self.action_insert_photo() | ||||
|                 event.stop() | ||||
|             elif event.key == "n": | ||||
| 
 | ||||
|                 self.action_ingest_new_photo() | ||||
|                 event.stop() | ||||
|             elif event.key == "d": | ||||
| 
 | ||||
|                 self.action_delete_photo() | ||||
|                 event.stop() | ||||
|             elif event.key == "e": | ||||
| 
 | ||||
|                 self.action_edit_photo() | ||||
|                 event.stop() | ||||
|         # Shift+Tab: remove indent | ||||
|  | @ -839,4 +1028,4 @@ class EditEntryScreen(Screen): | |||
|         # Tab: insert tab | ||||
|         elif self.focused is self.text_entry and event.key == "tab": | ||||
|             self.text_entry.insert('\t') | ||||
|             event.stop()  | ||||
|             event.stop() | ||||
|  | @ -5,6 +5,7 @@ from textual.screen import Screen | |||
| from textual.widgets import Static, Input, Button | ||||
| from textual.containers import Horizontal, Container | ||||
| from .file_picker_modal import FilePickerModal | ||||
| import hashlib | ||||
| 
 | ||||
| class AddPhotoModal(Screen): | ||||
|     """Modal for adding a new photo""" | ||||
|  | @ -12,6 +13,14 @@ class AddPhotoModal(Screen): | |||
|         super().__init__() | ||||
|         self.diary_id = diary_id | ||||
|         self.result = None | ||||
|         self.created_photo = None | ||||
| 
 | ||||
|     def _generate_photo_hash(self, photo_data: dict) -> str: | ||||
|         """Generate a short, unique hash for a photo""" | ||||
|         # Use temporary data for hash generation | ||||
|         unique_string = f"{photo_data['name']}_{photo_data.get('photo_id', 0)}_new" | ||||
|         hash_object = hashlib.md5(unique_string.encode()) | ||||
|         return hash_object.hexdigest()[:8] | ||||
| 
 | ||||
|     def compose(self) -> ComposeResult: | ||||
|         yield Container( | ||||
|  | @ -72,13 +81,23 @@ class AddPhotoModal(Screen): | |||
|             ) | ||||
| 
 | ||||
|             if new_photo: | ||||
|                 self.notify(f"Photo '{new_photo.name}' added successfully!") | ||||
|                 self.created_photo = new_photo | ||||
|                 # Generate hash for the new photo | ||||
|                 photo_hash = self._generate_photo_hash({ | ||||
|                     "name": new_photo.name, | ||||
|                     "photo_id": new_photo.id | ||||
|                 }) | ||||
|                  | ||||
|                 self.notify(f"Photo '{new_photo.name}' added successfully!\nHash: {photo_hash}\nReference: \\[\\[photo:{new_photo.name}:{photo_hash}\\]\\]",  | ||||
|                            severity="information", timeout=5) | ||||
|                  | ||||
|                 # 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 | ||||
|                     "photo_id": new_photo.id, | ||||
|                     "hash": photo_hash | ||||
|                 } | ||||
|                 self.dismiss(self.result) | ||||
|             else: | ||||
|  |  | |||
|  | @ -3,6 +3,7 @@ from textual.screen import Screen | |||
| from textual.widgets import Static, Input, Button | ||||
| from textual.containers import Container, Horizontal | ||||
| from pilgrim.models.photo import Photo | ||||
| import hashlib | ||||
| 
 | ||||
| class EditPhotoModal(Screen): | ||||
|     """Modal for editing an existing photo (name and caption only)""" | ||||
|  | @ -11,7 +12,16 @@ class EditPhotoModal(Screen): | |||
|         self.photo = photo | ||||
|         self.result = None | ||||
| 
 | ||||
|     def _generate_photo_hash(self, photo: Photo) -> str: | ||||
|         """Generate a short, unique hash for a photo""" | ||||
|         unique_string = f"{photo.name}_{photo.id}_{photo.addition_date}" | ||||
|         hash_object = hashlib.md5(unique_string.encode()) | ||||
|         return hash_object.hexdigest()[:8] | ||||
| 
 | ||||
|     def compose(self) -> ComposeResult: | ||||
|         # Generate hash for this photo | ||||
|         photo_hash = self._generate_photo_hash(self.photo) | ||||
|          | ||||
|         yield Container( | ||||
|             Static("✏️ Edit Photo", classes="EditPhotoModal-Title"), | ||||
|             Static("File path (read-only):", classes="EditPhotoModal-Label"), | ||||
|  | @ -35,6 +45,10 @@ class EditPhotoModal(Screen): | |||
|                 id="caption-input",  | ||||
|                 classes="EditPhotoModal-Input" | ||||
|             ), | ||||
|             Static(f"🔗 Photo Hash: {photo_hash}", classes="EditPhotoModal-Hash"), | ||||
|             Static("Reference formats:", classes="EditPhotoModal-Label"), | ||||
|             Static(f"\\[\\[photo:{self.photo.name}:{photo_hash}\\]\\]", classes="EditPhotoModal-Reference"), | ||||
|             Static(f"\\[\\[photo::{photo_hash}\\]\\]", classes="EditPhotoModal-Reference"), | ||||
|             Horizontal( | ||||
|                 Button("Save Changes", id="save-button", classes="EditPhotoModal-Button"), | ||||
|                 Button("Cancel", id="cancel-button", classes="EditPhotoModal-Button"), | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue