|
|
|
|
@ -61,7 +61,6 @@ class EditEntryScreen(Screen):
|
|
|
|
|
self._active_notification = None
|
|
|
|
|
self._notification_timer = None
|
|
|
|
|
self.references = []
|
|
|
|
|
self.cached_photos = []
|
|
|
|
|
|
|
|
|
|
# Main header
|
|
|
|
|
self.header = Header(name="Pilgrim v6", classes="EditEntryScreen-header")
|
|
|
|
|
@ -117,11 +116,69 @@ class EditEntryScreen(Screen):
|
|
|
|
|
self.footer = Footer(classes="EditEntryScreen-footer")
|
|
|
|
|
|
|
|
|
|
def _update_footer_context(self):
|
|
|
|
|
"""Force footer refresh to show updated bindings"""
|
|
|
|
|
"""Forces footer refresh to show updated bindings"""
|
|
|
|
|
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 the current cursor position for tooltip placement"""
|
|
|
|
|
"""Get current cursor position for tooltip placement"""
|
|
|
|
|
try:
|
|
|
|
|
# Get cursor position from text area
|
|
|
|
|
cursor_location = self.text_entry.cursor_location
|
|
|
|
|
@ -154,11 +211,11 @@ 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
|
|
|
|
|
@ -168,7 +225,7 @@ class EditEntryScreen(Screen):
|
|
|
|
|
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
|
|
|
|
|
@ -181,7 +238,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):
|
|
|
|
|
@ -211,7 +268,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):
|
|
|
|
|
@ -282,28 +339,28 @@ class EditEntryScreen(Screen):
|
|
|
|
|
def _update_sidebar_content(self):
|
|
|
|
|
"""Updates the sidebar content with photos for the current diary"""
|
|
|
|
|
try:
|
|
|
|
|
self._load_photos_for_diary(self.diary_id)
|
|
|
|
|
photos = self._load_photos_for_diary(self.diary_id)
|
|
|
|
|
|
|
|
|
|
# Clear existing options safely
|
|
|
|
|
self.photo_list.clear_options()
|
|
|
|
|
|
|
|
|
|
# Add the 'Ingest Photo' option at the top
|
|
|
|
|
# Add 'Ingest Photo' option at the top
|
|
|
|
|
self.photo_list.add_option("➕ Ingest Photo")
|
|
|
|
|
|
|
|
|
|
if not self.cached_photos:
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
# Add photos to the list with hash
|
|
|
|
|
for photo in self.cached_photos:
|
|
|
|
|
for photo in 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}\]")
|
|
|
|
|
photo_hash = self._generate_photo_hash(photo)
|
|
|
|
|
self.photo_list.add_option(f"📷 {photo.name} \\[{photo_hash}\\]")
|
|
|
|
|
|
|
|
|
|
self.photo_info.update(f"📸 {len(self.cached_photos)} photos in diary")
|
|
|
|
|
|
|
|
|
|
# Updated help a text with hash information
|
|
|
|
|
self.photo_info.update(f"📸 {len(photos)} photos in diary")
|
|
|
|
|
|
|
|
|
|
# Updated help text with hash information
|
|
|
|
|
help_text = (
|
|
|
|
|
"[b]⌨️ Sidebar Shortcuts[/b]\n"
|
|
|
|
|
"[b][green]i[/green][/b]: Insert photo into entry\n"
|
|
|
|
|
@ -324,26 +381,26 @@ class EditEntryScreen(Screen):
|
|
|
|
|
self.photo_info.update("Error loading photos")
|
|
|
|
|
self.help_text.update("Error loading sidebar content")
|
|
|
|
|
|
|
|
|
|
def _load_photos_for_diary(self, diary_id: int):
|
|
|
|
|
def _load_photos_for_diary(self, diary_id: int) -> List[Photo]:
|
|
|
|
|
"""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()
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
photos = [photo for photo in all_photos if photo.fk_travel_diary_id == diary_id]
|
|
|
|
|
photos.sort(key=lambda x: x.id)
|
|
|
|
|
return photos
|
|
|
|
|
except Exception as e:
|
|
|
|
|
self.notify(f"Error loading photos: {str(e)}")
|
|
|
|
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
def action_toggle_sidebar(self):
|
|
|
|
|
"""Toggles the sidebar visibility"""
|
|
|
|
|
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()
|
|
|
|
|
@ -361,7 +418,7 @@ class EditEntryScreen(Screen):
|
|
|
|
|
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)
|
|
|
|
|
@ -380,54 +437,53 @@ class EditEntryScreen(Screen):
|
|
|
|
|
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 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
|
|
|
|
|
|
|
|
|
|
self._load_photos_for_diary(self.diary_id)
|
|
|
|
|
if photo_index < 0 or photo_index >= len(self.cached_photos):
|
|
|
|
|
|
|
|
|
|
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 = self.cached_photos[photo_index]
|
|
|
|
|
photo_hash = selected_photo.photo_hash[:8]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
selected_photo = photos[photo_index]
|
|
|
|
|
photo_hash = self._generate_photo_hash(selected_photo)
|
|
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
|
|
|
|
|
# Insert at 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"
|
|
|
|
|
@ -437,9 +493,9 @@ class EditEntryScreen(Screen):
|
|
|
|
|
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")
|
|
|
|
|
|
|
|
|
|
@ -448,7 +504,7 @@ 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Open add photo modal
|
|
|
|
|
try:
|
|
|
|
|
self.notify("Trying to push the modal screen...")
|
|
|
|
|
@ -478,7 +534,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"],
|
|
|
|
|
@ -503,21 +559,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),
|
|
|
|
|
@ -527,16 +583,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 an adjusted index
|
|
|
|
|
# Get the selected photo with adjusted index
|
|
|
|
|
photos = self._load_photos_for_diary(self.diary_id)
|
|
|
|
|
photo_index = self.photo_list.highlighted - 1 # Adjust for 'Ingest Photo' at top
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if self.photo_list.highlighted is None or photo_index < 0 or photo_index >= len(photos):
|
|
|
|
|
self.notify("Photo no longer available", severity="error")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
selected_photo = photos[photo_index]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Schedule async deletion
|
|
|
|
|
self.call_later(self._async_delete_photo, selected_photo)
|
|
|
|
|
else:
|
|
|
|
|
@ -566,21 +622,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),
|
|
|
|
|
@ -596,13 +652,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)
|
|
|
|
|
|
|
|
|
|
@ -635,116 +691,24 @@ 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
|
|
|
|
|
|
|
|
|
|
# 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:
|
|
|
|
|
if not photos or event.option_index <= 0: # Skip 'Ingest Photo' option
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Adjust index because of 'Ingest Photo' at the top
|
|
|
|
|
photo_index = event.option_index - 1
|
|
|
|
|
if photo_index >= len(photos):
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
selected_photo = photos[photo_index]
|
|
|
|
|
photo_hash = selected_photo.photo_hash[:8]
|
|
|
|
|
photo_hash = self._generate_photo_hash(selected_photo)
|
|
|
|
|
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"
|
|
|
|
|
@ -755,7 +719,7 @@ class EditEntryScreen(Screen):
|
|
|
|
|
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:
|
|
|
|
|
@ -763,12 +727,12 @@ class EditEntryScreen(Screen):
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
# Check for 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
|
|
|
|
|
@ -780,7 +744,7 @@ class EditEntryScreen(Screen):
|
|
|
|
|
|
|
|
|
|
def on_focus(self, event) -> None:
|
|
|
|
|
"""Captures focus changes to update footer"""
|
|
|
|
|
# Check if the focus changed to/from sidebar
|
|
|
|
|
# Check if focus changed to/from sidebar
|
|
|
|
|
if hasattr(event.widget, 'id'):
|
|
|
|
|
if event.widget.id == "photo_list":
|
|
|
|
|
self.sidebar_focused = True
|
|
|
|
|
@ -894,42 +858,37 @@ class EditEntryScreen(Screen):
|
|
|
|
|
self._update_sub_header()
|
|
|
|
|
|
|
|
|
|
def action_save(self) -> None:
|
|
|
|
|
"""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()
|
|
|
|
|
"""Saves the current entry"""
|
|
|
|
|
self._get_all_references()
|
|
|
|
|
self._validate_references()
|
|
|
|
|
if self.is_new_entry:
|
|
|
|
|
content = self.text_entry.text.strip()
|
|
|
|
|
if not content:
|
|
|
|
|
self.notify("Empty entry cannot be saved")
|
|
|
|
|
return
|
|
|
|
|
# Passe a lista de fotos para o método de criação
|
|
|
|
|
self.call_later(self._async_create_entry, content, photos_to_link)
|
|
|
|
|
else:
|
|
|
|
|
# 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, 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()
|
|
|
|
|
# Schedule async creation
|
|
|
|
|
self.call_later(self._async_create_entry, content)
|
|
|
|
|
else:
|
|
|
|
|
# Schedule async update
|
|
|
|
|
self.call_later(self._async_update_entry)
|
|
|
|
|
|
|
|
|
|
async def _async_create_entry(self, content: str):
|
|
|
|
|
"""Creates a new entry asynchronously"""
|
|
|
|
|
try:
|
|
|
|
|
service_manager = self.app.service_manager
|
|
|
|
|
entry_service = service_manager.get_entry_service()
|
|
|
|
|
|
|
|
|
|
# O service.create deve criar o objeto em memória, mas NÃO fazer o commit ainda.
|
|
|
|
|
current_date = datetime.now()
|
|
|
|
|
|
|
|
|
|
new_entry = entry_service.create(
|
|
|
|
|
travel_diary_id=self.diary_id,
|
|
|
|
|
title=self.new_entry_title,
|
|
|
|
|
text=content,
|
|
|
|
|
date=datetime.now(),
|
|
|
|
|
photos=photos_to_link
|
|
|
|
|
date=current_date
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
@ -940,67 +899,94 @@ class EditEntryScreen(Screen):
|
|
|
|
|
|
|
|
|
|
self.is_new_entry = False
|
|
|
|
|
self.has_unsaved_changes = False
|
|
|
|
|
self._original_content = new_entry.text # Pode ser o texto com hashes curtos
|
|
|
|
|
self._original_content = new_entry.text
|
|
|
|
|
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}' Successfully saved")
|
|
|
|
|
self.notify(f"New entry '{new_entry.title}' saved successfully!")
|
|
|
|
|
else:
|
|
|
|
|
self.notify("❌ Error creating the Entry")
|
|
|
|
|
self.notify("Error creating entry")
|
|
|
|
|
|
|
|
|
|
except Exception as 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
|
|
|
|
|
self.notify(f"Error creating entry: {str(e)}")
|
|
|
|
|
|
|
|
|
|
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]
|
|
|
|
|
entry_result : Entry = Entry(
|
|
|
|
|
updated_content = self.text_entry.text
|
|
|
|
|
|
|
|
|
|
updated_entry = Entry(
|
|
|
|
|
title=current_entry.title,
|
|
|
|
|
text=updated_content,
|
|
|
|
|
photos=photos_to_link,
|
|
|
|
|
date=current_entry.date,
|
|
|
|
|
travel_diary_id=self.diary_id
|
|
|
|
|
|
|
|
|
|
travel_diary_id=current_entry.fk_travel_diary_id,
|
|
|
|
|
id=current_entry.id
|
|
|
|
|
)
|
|
|
|
|
entry_service.update(current_entry, entry_result)
|
|
|
|
|
|
|
|
|
|
# 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")
|
|
|
|
|
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")
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
# Desfaz as mudanças em caso de erro
|
|
|
|
|
self.notify(f"❌ Error on updating the 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):
|
|
|
|
|
|
|
|
|
|
print("DEBUG: on_key called with", event.key, "sidebar_focused:", self.sidebar_focused, "sidebar_visible:", self.sidebar_visible)
|
|
|
|
|
# Sidebar contextual shortcuts
|
|
|
|
|
if self.sidebar_focused and self.sidebar_visible:
|
|
|
|
|
|
|
|
|
|
print("DEBUG: Processing sidebar shortcut for key:", event.key)
|
|
|
|
|
if event.key == "i":
|
|
|
|
|
|
|
|
|
|
print("DEBUG: Calling action_insert_photo")
|
|
|
|
|
self.action_insert_photo()
|
|
|
|
|
event.stop()
|
|
|
|
|
elif event.key == "n":
|
|
|
|
|
|
|
|
|
|
print("DEBUG: Calling action_ingest_new_photo")
|
|
|
|
|
self.action_ingest_new_photo()
|
|
|
|
|
event.stop()
|
|
|
|
|
elif event.key == "d":
|
|
|
|
|
|
|
|
|
|
print("DEBUG: Calling action_delete_photo")
|
|
|
|
|
self.action_delete_photo()
|
|
|
|
|
event.stop()
|
|
|
|
|
elif event.key == "e":
|
|
|
|
|
|
|
|
|
|
print("DEBUG: Calling action_edit_photo")
|
|
|
|
|
self.action_edit_photo()
|
|
|
|
|
event.stop()
|
|
|
|
|
# Shift+Tab: remove indent
|
|
|
|
|
@ -1028,4 +1014,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()
|