Improved the photo sidebar and add the add_photo_modal.py to add photos and the file_picker_modal.py to select the proper file to be ingested, also changed photo.py and photo_service.py to receive a datatime and not a string representing the addition date

This commit is contained in:
Gustavo Henrique Santos Souza de Miranda
2025-06-29 21:38:33 -03:00
parent 84b0397263
commit f84da2c934
10 changed files with 226 additions and 58 deletions
+62 -23
View File
@@ -13,6 +13,8 @@ from pilgrim.models.entry import Entry
from pilgrim.models.travel_diary import TravelDiary
from pilgrim.models.photo import Photo
from pilgrim.ui.screens.modals.add_photo_modal import AddPhotoModal
from pilgrim.ui.screens.modals.edit_photo_modal import EditPhotoModal
from pilgrim.ui.screens.modals.confirm_delete_modal import ConfirmDeleteModal
from pilgrim.ui.screens.modals.file_picker_modal import FilePickerModal
from pilgrim.ui.screens.rename_entry_modal import RenameEntryModal
@@ -275,7 +277,7 @@ class EditEntryScreen(Screen):
"[b][green]e[/green][/b]: Edit selected photo\n"
"[b][yellow]Tab[/yellow][/b]: Back to editor\n"
"[b][yellow]F8[/yellow][/b]: Show/hide sidebar\n"
"[b][yellow]F9[/yellow][/b]: Focus Sidebar/Editor"
"[b][yellow]F9[/yellow][/b]: Switch focus (if needed)"
)
self.help_text.update(help_text)
@@ -301,10 +303,13 @@ class EditEntryScreen(Screen):
if self.sidebar_visible:
self.sidebar.display = True
self._update_sidebar_content()
# Automatically focus the sidebar when opening
self.sidebar_focused = True
self.photo_list.focus()
# Notification when opening the sidebar for the first time
if not self._sidebar_opened_once:
self.notify(
"Sidebar opened! Context-specific shortcuts are always visible in the sidebar help panel.",
"Sidebar opened and focused! Use the shortcuts shown in the help panel.",
severity="info"
)
self._sidebar_opened_once = True
@@ -337,7 +342,7 @@ class EditEntryScreen(Screen):
def action_insert_photo(self):
"""Insert selected photo into text"""
if not self.sidebar_focused or not self.sidebar_visible:
self.notify("Use F9 to focus the sidebar before using this shortcut.", severity="warning")
self.notify("Use F8 to open the sidebar first.", severity="warning")
return
# Get selected photo
@@ -345,11 +350,15 @@ class EditEntryScreen(Screen):
self.notify("No photo selected", severity="warning")
return
# Adjust index because of 'Ingest Photo' at the top
photo_index = self.photo_list.highlighted - 1
photos = self._load_photos_for_diary(self.diary_id)
if self.photo_list.highlighted >= len(photos):
if photo_index < 0 or photo_index >= len(photos):
self.notify("No photo selected", severity="warning")
return
selected_photo = photos[self.photo_list.highlighted]
selected_photo = photos[photo_index]
# Insert photo reference into text
photo_ref = f"\n[📷 {selected_photo.name}]({selected_photo.filepath})\n"
@@ -367,7 +376,7 @@ class EditEntryScreen(Screen):
def action_ingest_new_photo(self):
"""Ingest a new photo using modal"""
if not self.sidebar_focused or not self.sidebar_visible:
self.notify("Use F9 to focus the sidebar before using this shortcut.", severity="warning")
self.notify("Use F8 to open the sidebar first.", severity="warning")
return
# Open add photo modal
@@ -382,8 +391,10 @@ class EditEntryScreen(Screen):
self.notify("Add photo cancelled")
return
# Schedule async creation
self.call_later(self._async_create_photo, result)
# Photo was already created in the modal, just refresh the sidebar
if self.sidebar_visible:
self._update_sidebar_content()
self.notify(f"Photo '{result['name']}' added successfully!")
async def _async_create_photo(self, photo_data: dict):
"""Creates a new photo asynchronously"""
@@ -415,24 +426,46 @@ class EditEntryScreen(Screen):
def action_delete_photo(self):
"""Delete selected photo"""
if not self.sidebar_focused or not self.sidebar_visible:
self.notify("Use F9 to focus the sidebar before using this shortcut.", severity="warning")
self.notify("Use F8 to open the sidebar first.", severity="warning")
return
if self.photo_list.highlighted is None:
self.notify("No photo selected", severity="warning")
return
# Adjust index because of 'Ingest Photo' at the top
photo_index = self.photo_list.highlighted - 1
photos = self._load_photos_for_diary(self.diary_id)
if self.photo_list.highlighted >= len(photos):
if photo_index < 0 or photo_index >= len(photos):
self.notify("No photo selected", severity="warning")
return
selected_photo = photos[self.photo_list.highlighted]
selected_photo = photos[photo_index]
# Confirm deletion
self.notify(f"Deleting photo: {selected_photo.name}")
# Schedule async deletion
self.call_later(self._async_delete_photo, selected_photo)
# Open confirm delete modal
self.app.push_screen(
ConfirmDeleteModal(photo=selected_photo),
self.handle_delete_photo_result
)
def handle_delete_photo_result(self, result: bool) -> None:
"""Callback that processes the delete photo modal result."""
if result:
# Get the selected photo with adjusted index
photos = self._load_photos_for_diary(self.diary_id)
photo_index = self.photo_list.highlighted - 1 # Adjust for 'Ingest Photo' at top
if self.photo_list.highlighted is None or photo_index < 0 or photo_index >= len(photos):
self.notify("Photo no longer available", severity="error")
return
selected_photo = photos[photo_index]
# Schedule async deletion
self.call_later(self._async_delete_photo, selected_photo)
else:
self.notify("Delete cancelled")
async def _async_delete_photo(self, photo: Photo):
"""Deletes a photo asynchronously"""
@@ -456,18 +489,22 @@ class EditEntryScreen(Screen):
def action_edit_photo(self):
"""Edit selected photo using modal"""
if not self.sidebar_focused or not self.sidebar_visible:
self.notify("Use F9 to focus the sidebar before using this shortcut.", severity="warning")
self.notify("Use F8 to open the sidebar first.", severity="warning")
return
if self.photo_list.highlighted is None:
self.notify("No photo selected", severity="warning")
return
# Adjust index because of 'Ingest Photo' at the top
photo_index = self.photo_list.highlighted - 1
photos = self._load_photos_for_diary(self.diary_id)
if self.photo_list.highlighted >= len(photos):
if photo_index < 0 or photo_index >= len(photos):
self.notify("No photo selected", severity="warning")
return
selected_photo = photos[self.photo_list.highlighted]
selected_photo = photos[photo_index]
# Open edit photo modal
self.app.push_screen(
@@ -481,13 +518,15 @@ class EditEntryScreen(Screen):
self.notify("Edit photo cancelled")
return
# Get the selected photo
# Get the selected photo with adjusted index
photos = self._load_photos_for_diary(self.diary_id)
if self.photo_list.highlighted is None or self.photo_list.highlighted >= len(photos):
photo_index = self.photo_list.highlighted - 1 # Adjust for 'Ingest Photo' at top
if self.photo_list.highlighted is None or photo_index < 0 or photo_index >= len(photos):
self.notify("Photo no longer available", severity="error")
return
selected_photo = photos[self.photo_list.highlighted]
selected_photo = photos[photo_index]
# Schedule async update
self.call_later(self._async_update_photo, selected_photo, result)
@@ -504,7 +543,7 @@ class EditEntryScreen(Screen):
name=photo_data["name"],
addition_date=original_photo.addition_date,
caption=photo_data["caption"],
entries=original_photo.entries,
entries=original_photo.entries if original_photo.entries is not None else [],
id=original_photo.id
)
@@ -1,4 +1,5 @@
import os
from pathlib import Path
from textual.app import ComposeResult
from textual.screen import Screen
from textual.widgets import Static, Input, Button
@@ -47,15 +48,58 @@ class AddPhotoModal(Screen):
if not filepath.strip() or not name.strip():
self.notify("File path and name are required", severity="error")
return
self.result = {
# Try to create the photo in the database
self.call_later(self._async_create_photo, {
"filepath": filepath.strip(),
"name": name.strip(),
"caption": caption.strip() if caption.strip() else None
}
self.dismiss()
})
elif event.button.id == "cancel-button":
self.dismiss()
async def _async_create_photo(self, photo_data: dict):
"""Creates a new photo asynchronously using PhotoService"""
try:
service_manager = self.app.service_manager
photo_service = service_manager.get_photo_service()
new_photo = photo_service.create(
filepath=Path(photo_data["filepath"]),
name=photo_data["name"],
travel_diary_id=self.diary_id,
caption=photo_data["caption"]
)
if new_photo:
self.notify(f"Photo '{new_photo.name}' added successfully!")
# Return the created photo data to the calling screen
self.result = {
"filepath": photo_data["filepath"],
"name": photo_data["name"],
"caption": photo_data["caption"],
"photo_id": new_photo.id
}
self.dismiss(self.result)
else:
self.notify("Error creating photo in database", severity="error")
except Exception as e:
self.notify(f"Error creating photo: {str(e)}", severity="error")
def handle_file_picker_result(self, result: str | None) -> None:
if result:
self.query_one("#filepath-input", Input).value = result
# Set the filepath input value
filepath_input = self.query_one("#filepath-input", Input)
filepath_input.value = result
# Trigger the input change event to update the UI
filepath_input.refresh()
# Auto-fill the name field with the filename (without extension)
filename = Path(result).stem
name_input = self.query_one("#name-input", Input)
if not name_input.value.strip():
name_input.value = filename
name_input.refresh()
else:
# User cancelled the file picker
self.notify("File selection cancelled", severity="information")
@@ -25,7 +25,6 @@ class FilePickerModal(Screen):
self.start_path = Path(start_path or os.getcwd())
# Start one level up to make navigation easier
self.current_path = self.start_path.parent
self.result = None
def compose(self) -> ComposeResult:
yield Container(
@@ -45,8 +44,8 @@ class FilePickerModal(Screen):
# Check if it's an image file
image_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'}
if file_path.suffix.lower() in image_extensions:
self.result = str(file_path)
self.dismiss()
# Return the file path as result
self.dismiss(str(file_path))
else:
self.notify("Please select an image file", severity="warning")
@@ -63,4 +62,5 @@ class FilePickerModal(Screen):
tree.path = str(self.current_path)
tree.reload()
elif event.button.id == "cancel-button":
self.dismiss()
# Return None to indicate cancellation
self.dismiss(None)
+38
View File
@@ -588,4 +588,42 @@ Screen.-modal {
height: 1fr;
border: solid $accent;
margin: 1;
}
/* ConfirmDeleteModal styles */
.ConfirmDeleteModal-Dialog {
layout: vertical;
width: 60%;
height: auto;
background: $surface;
border: thick $error;
padding: 2 4;
align: center middle;
}
.ConfirmDeleteModal-Title {
text-align: center;
text-style: bold;
color: $error;
margin-bottom: 1;
}
.ConfirmDeleteModal-Message {
text-align: center;
color: $text;
margin-bottom: 1;
}
.ConfirmDeleteModal-Warning {
text-align: center;
color: $warning;
text-style: italic;
margin-bottom: 2;
}
.ConfirmDeleteModal-Buttons {
width: 1fr;
height: auto;
align: center middle;
padding-top: 1;
}
.ConfirmDeleteModal-Button {
margin: 0 1;
width: 1fr;
}