mirror of https://github.com/gmbrax/Pilgrim.git
commit
5b7b147e43
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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"),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue