Merge pull request #14

feat/photo-reference-system
This commit is contained in:
Gustavo Henrique Miranda 2025-07-03 21:10:26 -03:00 committed by GitHub
commit 5b7b147e43
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 308 additions and 86 deletions

View File

@ -16,6 +16,7 @@ class Photo(Base):
name = Column(String) name = Column(String)
addition_date = Column(DateTime, default=datetime.now) addition_date = Column(DateTime, default=datetime.now)
caption = Column(String) caption = Column(String)
photo_hash = Column(String,name='hash')
entries = relationship( entries = relationship(
"Entry", "Entry",
secondary=photo_entry_association, secondary=photo_entry_association,
@ -24,7 +25,7 @@ class Photo(Base):
fk_travel_diary_id = Column(Integer, ForeignKey("travel_diaries.id"),nullable=False) 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) super().__init__(**kw)
# Convert Path to string if needed # Convert Path to string if needed
if isinstance(filepath, Path): if isinstance(filepath, Path):
@ -34,6 +35,7 @@ class Photo(Base):
self.name = name self.name = name
self.addition_date = addition_date if addition_date is not None else datetime.now() self.addition_date = addition_date if addition_date is not None else datetime.now()
self.caption = caption self.caption = caption
self.photo_hash = photo_hash
self.entries = entries if entries is not None else [] self.entries = entries if entries is not None else []
if fk_travel_diary_id is not None: if fk_travel_diary_id is not None:
self.fk_travel_diary_id = fk_travel_diary_id self.fk_travel_diary_id = fk_travel_diary_id

View File

@ -1,6 +1,7 @@
from pathlib import Path from pathlib import Path
from typing import List from typing import List
from datetime import datetime from datetime import datetime
import hashlib
from pilgrim.models.photo import Photo from pilgrim.models.photo import Photo
@ -9,6 +10,12 @@ from pilgrim.models.travel_diary import TravelDiary
class PhotoService: class PhotoService:
def __init__(self, session): def __init__(self, session):
self.session = 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: 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() travel_diary = self.session.query(TravelDiary).filter(TravelDiary.id == travel_diary_id).first()
@ -27,7 +34,8 @@ class PhotoService:
name=name, name=name,
caption=caption, caption=caption,
fk_travel_diary_id=travel_diary_id, 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.add(new_photo)
self.session.commit() self.session.commit()
@ -47,6 +55,7 @@ class PhotoService:
original.name = photo_dst.name original.name = photo_dst.name
original.addition_date = photo_dst.addition_date original.addition_date = photo_dst.addition_date
original.caption = photo_dst.caption original.caption = photo_dst.caption
original.photo_hash = original.photo_hash
if photo_dst.entries and len(photo_dst.entries) > 0: if photo_dst.entries and len(photo_dst.entries) > 0:
if original.entries is None: if original.entries is None:
original.entries = [] original.entries = []
@ -66,7 +75,10 @@ class PhotoService:
addition_date=excluded.addition_date, addition_date=excluded.addition_date,
caption=excluded.caption, caption=excluded.caption,
fk_travel_diary_id=excluded.fk_travel_diary_id, 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) self.session.delete(excluded)

View File

@ -2,6 +2,9 @@ from typing import Optional, List
import asyncio import asyncio
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
import hashlib
import re
import time
from textual.app import ComposeResult from textual.app import ComposeResult
from textual.screen import Screen from textual.screen import Screen
@ -23,13 +26,15 @@ class EditEntryScreen(Screen):
TITLE = "Pilgrim - Edit" TITLE = "Pilgrim - Edit"
BINDINGS = [ BINDINGS = [
Binding("ctrl+s", "save", "Save"), ("ctrl+q", "quit", "Quit"),
Binding("ctrl+n", "next_entry", "Next/New Entry"), ("ctrl+s", "save", "Save"),
Binding("ctrl+b", "prev_entry", "Previous Entry"), ("ctrl+n", "new_entry", "New Entry"),
Binding("ctrl+r", "rename_entry", "Rename Entry"), ("ctrl+shift+n", "next_entry", "Next Entry"),
Binding("escape", "back_to_list", "Back to List"), ("ctrl+shift+p", "prev_entry", "Previous Entry"),
Binding("f8", "toggle_sidebar", "Toggle Sidebar"), ("ctrl+r", "rename_entry", "Rename Entry"),
Binding("f9", "toggle_focus", "Focus Sidebar/Editor"), ("f8", "toggle_sidebar", "Toggle Photos"),
("f9", "toggle_focus", "Toggle Focus"),
("escape", "back_to_list", "Back to List"),
] ]
def __init__(self, diary_id: int = 1): def __init__(self, diary_id: int = 1):
@ -50,6 +55,12 @@ class EditEntryScreen(Screen):
self.sidebar_visible = False self.sidebar_visible = False
self.sidebar_focused = False self.sidebar_focused = False
self._sidebar_opened_once = 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 = []
# Main header # Main header
self.header = Header(name="Pilgrim v6", classes="EditEntryScreen-header") self.header = Header(name="Pilgrim v6", classes="EditEntryScreen-header")
@ -108,6 +119,83 @@ class EditEntryScreen(Screen):
"""Forces footer refresh to show updated bindings""" """Forces footer refresh to show updated bindings"""
self.refresh() self.refresh()
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 _fuzzy_search(self, query: str, photos: List[Photo]) -> List[Photo]:
"""Fuzzy search for photos by name or hash"""
if not query:
return []
query = query.lower()
results = []
for photo in photos:
photo_hash = self._generate_photo_hash(photo)
photo_name = photo.name.lower()
# Check if query is in name (substring match)
if query in photo_name:
results.append((photo, 1, f"Name match: {query} in {photo.name}"))
continue
# Check if query is in hash (substring match)
if query in photo_hash:
results.append((photo, 2, f"Hash match: {query} in {photo_hash}"))
continue
# Fuzzy match for name (check if all characters are present in order)
if self._fuzzy_match(query, photo_name):
results.append((photo, 3, f"Fuzzy name match: {query} in {photo.name}"))
continue
# Fuzzy match for hash
if self._fuzzy_match(query, photo_hash):
results.append((photo, 4, f"Fuzzy hash match: {query} in {photo_hash}"))
continue
# Sort by priority (lower number = higher priority)
results.sort(key=lambda x: x[1])
return [photo for photo, _, _ in results]
def _fuzzy_match(self, query: str, text: str) -> bool:
"""Check if query characters appear in text in order (fuzzy match)"""
if not query:
return True
query_idx = 0
for char in text:
if query_idx < len(query) and char == query[query_idx]:
query_idx += 1
if query_idx == len(query):
return True
return False
def _get_cursor_position(self) -> tuple:
"""Get 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: def compose(self) -> ComposeResult:
print("DEBUG: EditEntryScreen COMPOSE", getattr(self, 'sidebar_visible', None)) print("DEBUG: EditEntryScreen COMPOSE", getattr(self, 'sidebar_visible', None))
yield self.header yield self.header
@ -130,6 +218,7 @@ class EditEntryScreen(Screen):
# Initialize footer with editor context # Initialize footer with editor context
self._update_footer_context() self._update_footer_context()
# self.app.mount(self._photo_suggestion_widget) # Temporarily disabled
def update_diary_info(self): def update_diary_info(self):
"""Updates diary information""" """Updates diary information"""
@ -249,37 +338,48 @@ class EditEntryScreen(Screen):
def _update_sidebar_content(self): def _update_sidebar_content(self):
"""Updates the sidebar content with photos for the current diary""" """Updates the sidebar content with photos for the current diary"""
photos = self._load_photos_for_diary(self.diary_id) try:
photos = self._load_photos_for_diary(self.diary_id)
# Clear existing options safely # Clear existing options safely
self.photo_list.clear_options() self.photo_list.clear_options()
# Add 'Ingest Photo' option at the top # Add 'Ingest Photo' option at the top
self.photo_list.add_option(" Ingest Photo") self.photo_list.add_option(" Ingest Photo")
if not photos: if not photos:
self.photo_info.update("No photos found for this diary") self.photo_info.update("No photos found for this diary")
self.help_text.update("📸 No photos available\n\nUse Photo Manager to add photos") self.help_text.update("📸 No photos available\n\nUse Photo Manager to add photos")
return return
# Add photos to the list # Add photos to the list with hash
for photo in photos: for photo in photos:
self.photo_list.add_option(f"📷 {photo.name}") # Show name and hash in the list
photo_hash = self._generate_photo_hash(photo)
self.photo_list.add_option(f"📷 {photo.name} \\[{photo_hash}\\]")
self.photo_info.update(f"📸 {len(photos)} photos in diary") self.photo_info.update(f"📸 {len(photos)} photos in diary")
# English, visually distinct help text # Updated help text with hash information
help_text = ( help_text = (
"[b]⌨️ Sidebar Shortcuts[/b]\n" "[b]⌨️ Sidebar Shortcuts[/b]\n"
"[b][green]i[/green][/b]: Insert photo into entry\n" "[b][green]i[/green][/b]: Insert photo into entry\n"
"[b][green]n[/green][/b]: Add new photo\n" "[b][green]n[/green][/b]: Add new photo\n"
"[b][green]d[/green][/b]: Delete selected photo\n" "[b][green]d[/green][/b]: Delete selected photo\n"
"[b][green]e[/green][/b]: Edit selected photo\n" "[b][green]e[/green][/b]: Edit selected photo\n"
"[b][yellow]Tab[/yellow][/b]: Back to editor\n" "[b][yellow]Tab[/yellow][/b]: Back to editor\n"
"[b][yellow]F8[/yellow][/b]: Show/hide sidebar\n" "[b][yellow]F8[/yellow][/b]: Show/hide sidebar\n"
"[b][yellow]F9[/yellow][/b]: Switch focus (if needed)" "[b][yellow]F9[/yellow][/b]: Switch focus (if needed)\n\n"
) "[b]📝 Photo References[/b]\n"
self.help_text.update(help_text) "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) -> List[Photo]: def _load_photos_for_diary(self, diary_id: int) -> List[Photo]:
"""Loads all photos for the specific diary""" """Loads all photos for the specific diary"""
@ -297,40 +397,49 @@ class EditEntryScreen(Screen):
def action_toggle_sidebar(self): def action_toggle_sidebar(self):
"""Toggles the sidebar visibility""" """Toggles the sidebar visibility"""
print("DEBUG: TOGGLE SIDEBAR", self.sidebar_visible) try:
self.sidebar_visible = not self.sidebar_visible print("DEBUG: TOGGLE SIDEBAR", self.sidebar_visible)
self.sidebar_visible = not self.sidebar_visible
if self.sidebar_visible:
self.sidebar.display = True if self.sidebar_visible:
self._update_sidebar_content() self.sidebar.display = True
# Automatically focus the sidebar when opening self._update_sidebar_content()
self.sidebar_focused = True # Automatically focus the sidebar when opening
self.photo_list.focus() self.sidebar_focused = True
# Notification when opening the sidebar for the first time self.photo_list.focus()
if not self._sidebar_opened_once: # Notification when opening the sidebar for the first time
self.notify( if not self._sidebar_opened_once:
"Sidebar opened and focused! Use the shortcuts shown in the help panel.", self.notify(
severity="info" "Sidebar opened and focused! Use the shortcuts shown in the help panel.",
) severity="info"
self._sidebar_opened_once = True )
else: 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.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): def action_toggle_focus(self):
"""Toggles focus between editor and sidebar""" """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 not self.sidebar_visible:
# If sidebar is not visible, show it and focus it # If sidebar is not visible, show it and focus it
print("DEBUG: Sidebar not visible, opening it")
self.action_toggle_sidebar() self.action_toggle_sidebar()
return return
self.sidebar_focused = not self.sidebar_focused self.sidebar_focused = not self.sidebar_focused
print("DEBUG: Sidebar focused changed to", self.sidebar_focused)
if self.sidebar_focused: if self.sidebar_focused:
self.photo_list.focus() self.photo_list.focus()
else: else:
@ -345,7 +454,7 @@ class EditEntryScreen(Screen):
self.notify("Use F8 to open the sidebar first.", severity="warning") self.notify("Use F8 to open the sidebar first.", severity="warning")
return return
# Get selected photo # Get a selected photo
if self.photo_list.highlighted is None: if self.photo_list.highlighted is None:
self.notify("No photo selected", severity="warning") self.notify("No photo selected", severity="warning")
return return
@ -359,19 +468,36 @@ class EditEntryScreen(Screen):
return return
selected_photo = photos[photo_index] selected_photo = photos[photo_index]
photo_hash = self._generate_photo_hash(selected_photo)
# Insert photo reference into text # Insert photo reference using hash format without escaping
photo_ref = f"\n[📷 {selected_photo.name}]({selected_photo.filepath})\n" # Using raw string to avoid markup conflicts with [[
if selected_photo.caption: photo_ref = f"[[photo::{photo_hash}]]"
photo_ref += f"*{selected_photo.caption}*\n"
# Insert at cursor position or at end # Insert at cursor position
current_text = self.text_entry.text self.text_entry.insert(photo_ref)
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}") # 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): def action_ingest_new_photo(self):
"""Ingest a new photo using modal""" """Ingest a new photo using modal"""
@ -380,10 +506,15 @@ class EditEntryScreen(Screen):
return return
# Open add photo modal # Open add photo modal
self.app.push_screen( try:
AddPhotoModal(diary_id=self.diary_id), self.notify("Trying to push the modal screen...")
self.handle_add_photo_result 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: def handle_add_photo_result(self, result: dict | None) -> None:
"""Callback that processes the add photo modal result.""" """Callback that processes the add photo modal result."""
@ -564,30 +695,44 @@ class EditEntryScreen(Screen):
"""Handles photo selection in the sidebar""" """Handles photo selection in the sidebar"""
if not self.sidebar_visible: if not self.sidebar_visible:
return return
# If 'Ingest Photo' is selected (always index 0)
if event.option_index == 0:
self.action_ingest_new_photo()
return
photos = self._load_photos_for_diary(self.diary_id) photos = self._load_photos_for_diary(self.diary_id)
if not photos or event.option_index <= 0: # Skip 'Ingest Photo' option
return
# Adjust index because of 'Ingest Photo' at the top # Adjust index because of 'Ingest Photo' at the top
photo_index = event.option_index - 1 photo_index = event.option_index - 1
if not photos or photo_index >= len(photos): if photo_index >= len(photos):
return return
selected_photo = photos[photo_index] selected_photo = photos[photo_index]
self.notify(f"Selected photo: {selected_photo.name}") photo_hash = self._generate_photo_hash(selected_photo)
# Update photo info with details 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"📷 {selected_photo.name}\n"
photo_details += f"🔗 {photo_hash}\n"
photo_details += f"📅 {selected_photo.addition_date}\n" photo_details += f"📅 {selected_photo.addition_date}\n"
if selected_photo.caption: if selected_photo.caption:
photo_details += f"💬 {selected_photo.caption}\n" 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) self.photo_info.update(photo_details)
def on_text_area_changed(self, event) -> None: 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 if (hasattr(self, 'text_entry') and not self.text_entry.read_only and
not getattr(self, '_updating_display', False) and hasattr(self, '_original_content')): not getattr(self, '_updating_display', False) and hasattr(self, '_original_content')):
current_content = self.text_entry.text current_content = self.text_entry.text
# Check for photo reference pattern
# self._check_photo_reference(current_content) # Temporarily disabled
if current_content != self._original_content: if current_content != self._original_content:
if not self.has_unsaved_changes: if not self.has_unsaved_changes:
self.has_unsaved_changes = True self.has_unsaved_changes = True
@ -714,6 +859,8 @@ class EditEntryScreen(Screen):
def action_save(self) -> None: def action_save(self) -> None:
"""Saves the current entry""" """Saves the current entry"""
self._get_all_references()
self._validate_references()
if self.is_new_entry: if self.is_new_entry:
content = self.text_entry.text.strip() content = self.text_entry.text.strip()
if not content: if not content:
@ -798,20 +945,48 @@ class EditEntryScreen(Screen):
except Exception as e: except Exception as e:
self.notify(f"Error updating entry: {str(e)}") self.notify(f"Error updating entry: {str(e)}")
def _get_all_references(self):
text_content = self.text_entry.text
matches = re.findall("(\[\[photo::?(?:\w|\s)*\]\])", text_content)
for match in matches:
if re.match(r"\[\[photo::\w+\]\]", match):
if {'type': 'hash','value':match.replace("[[photo::", "").replace("]]", "").strip()} not in self.references:
self.references.append(
{'type': 'hash', 'value': match.replace("[[photo::", "").replace("]]", "").strip()})
elif re.match(r"\[\[photo:\w+\]\]", match):
if {'type': 'name', 'value': match.replace("[[photo:", "").replace("]]", "").strip()} not in self.references:
self.references.append(
{'type': 'name', 'value': match.replace("[[photo:", "").replace("]]", "").strip()})
else:
self.references.append({'type': 'unknown', 'value': match})
self.notify(f"🔍 Referências encontradas: {str(self.references)}", markup=False)
def _validate_references(self):
for reference in self.references:
if reference['type'] == 'hash':
self.notify("hash")
elif reference['type'] == 'name':
self.notify("name")
def on_key(self, event): def on_key(self, event):
print("DEBUG: on_key called with", event.key, "sidebar_focused:", self.sidebar_focused, "sidebar_visible:", self.sidebar_visible)
# Sidebar contextual shortcuts # Sidebar contextual shortcuts
if self.sidebar_focused and self.sidebar_visible: if self.sidebar_focused and self.sidebar_visible:
print("DEBUG: Processing sidebar shortcut for key:", event.key)
if event.key == "i": if event.key == "i":
print("DEBUG: Calling action_insert_photo")
self.action_insert_photo() self.action_insert_photo()
event.stop() event.stop()
elif event.key == "n": elif event.key == "n":
print("DEBUG: Calling action_ingest_new_photo")
self.action_ingest_new_photo() self.action_ingest_new_photo()
event.stop() event.stop()
elif event.key == "d": elif event.key == "d":
print("DEBUG: Calling action_delete_photo")
self.action_delete_photo() self.action_delete_photo()
event.stop() event.stop()
elif event.key == "e": elif event.key == "e":
print("DEBUG: Calling action_edit_photo")
self.action_edit_photo() self.action_edit_photo()
event.stop() event.stop()
# Shift+Tab: remove indent # Shift+Tab: remove indent

View File

@ -5,6 +5,7 @@ from textual.screen import Screen
from textual.widgets import Static, Input, Button from textual.widgets import Static, Input, Button
from textual.containers import Horizontal, Container from textual.containers import Horizontal, Container
from .file_picker_modal import FilePickerModal from .file_picker_modal import FilePickerModal
import hashlib
class AddPhotoModal(Screen): class AddPhotoModal(Screen):
"""Modal for adding a new photo""" """Modal for adding a new photo"""
@ -12,6 +13,14 @@ class AddPhotoModal(Screen):
super().__init__() super().__init__()
self.diary_id = diary_id self.diary_id = diary_id
self.result = None 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: def compose(self) -> ComposeResult:
yield Container( yield Container(
@ -72,13 +81,23 @@ class AddPhotoModal(Screen):
) )
if new_photo: 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 # Return the created photo data to the calling screen
self.result = { self.result = {
"filepath": photo_data["filepath"], "filepath": photo_data["filepath"],
"name": photo_data["name"], "name": photo_data["name"],
"caption": photo_data["caption"], "caption": photo_data["caption"],
"photo_id": new_photo.id "photo_id": new_photo.id,
"hash": photo_hash
} }
self.dismiss(self.result) self.dismiss(self.result)
else: else:

View File

@ -3,6 +3,7 @@ from textual.screen import Screen
from textual.widgets import Static, Input, Button from textual.widgets import Static, Input, Button
from textual.containers import Container, Horizontal from textual.containers import Container, Horizontal
from pilgrim.models.photo import Photo from pilgrim.models.photo import Photo
import hashlib
class EditPhotoModal(Screen): class EditPhotoModal(Screen):
"""Modal for editing an existing photo (name and caption only)""" """Modal for editing an existing photo (name and caption only)"""
@ -11,7 +12,16 @@ class EditPhotoModal(Screen):
self.photo = photo self.photo = photo
self.result = None 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: def compose(self) -> ComposeResult:
# Generate hash for this photo
photo_hash = self._generate_photo_hash(self.photo)
yield Container( yield Container(
Static("✏️ Edit Photo", classes="EditPhotoModal-Title"), Static("✏️ Edit Photo", classes="EditPhotoModal-Title"),
Static("File path (read-only):", classes="EditPhotoModal-Label"), Static("File path (read-only):", classes="EditPhotoModal-Label"),
@ -35,6 +45,10 @@ class EditPhotoModal(Screen):
id="caption-input", id="caption-input",
classes="EditPhotoModal-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( Horizontal(
Button("Save Changes", id="save-button", classes="EditPhotoModal-Button"), Button("Save Changes", id="save-button", classes="EditPhotoModal-Button"),
Button("Cancel", id="cancel-button", classes="EditPhotoModal-Button"), Button("Cancel", id="cancel-button", classes="EditPhotoModal-Button"),