diff --git a/.gitignore b/.gitignore
index ca05f50..629a919 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,146 @@
+# Database files
database.db
-__pycache__
-/.idea/
+
+.build-vend/
+dist_nuitka/
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+build/
+temp/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+.python-version
+
+# pipenv
+Pipfile.lock
+
+# poetry
+poetry.lock
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.env.*
+.venv
+venv/
+ENV/
+env/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# IDE settings
+.vscode/
+.idea/
diff --git a/.idea/Pilgrim.iml b/.idea/Pilgrim.iml
deleted file mode 100644
index 3ce9a18..0000000
--- a/.idea/Pilgrim.iml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
deleted file mode 100644
index 105ce2d..0000000
--- a/.idea/inspectionProfiles/profiles_settings.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index daedced..0000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 94a25f7..0000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
deleted file mode 100644
index cd36295..0000000
--- a/.idea/workspace.xml
+++ /dev/null
@@ -1,200 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {
- "lastFilter": {
- "state": "OPEN",
- "assignee": "gmbrax"
- }
-}
- {
- "selectedUrlAndAccountId": {
- "url": "https://github.com/gmbrax/Pilgrim.git",
- "accountId": "213d8456-c67d-4cfd-99a6-337d47c35b4a"
- }
-}
- {
- "associatedIndex": 0
-}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 1748985568579
-
-
- 1748985568579
-
-
-
-
-
-
- 1749004109515
-
-
-
- 1749004109515
-
-
-
- 1749006784623
-
-
-
- 1749006784623
-
-
-
- 1749140898576
-
-
-
- 1749140898576
-
-
-
- 1749155713848
-
-
-
- 1749155713848
-
-
-
- 1749164385581
-
-
-
- 1749164385581
-
-
-
- 1749168650225
-
-
-
- 1749168650225
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/README.md b/README.md
index d9baacb..7d8d17a 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,50 @@
# Python_Pilgrim
-Python Based Travel Diary
\ No newline at end of file
+## Overview
+
+**Python_Pilgrim** is a Python-based travel diary application designed to help users document and manage their travel experiences. The project provides tools for recording trips, organizing travel notes, and storing memories in a structured and accessible format.
+
+## Features
+
+- Create and manage travel diaries
+- Add, edit, and delete travel entries
+- Organize trips by date, location, or theme
+- Store photos, notes, and other media
+- Export and share travel logs
+
+## Requirements
+- Python 3.8 or higher
+- Linux operating system (tested on Ubuntu 20.04+)
+- Visual Studio Code (VSCode) for development (optional but strongly recommended)
+- pip (Python package installer)
+- Optional: virtualenv for isolated environments
+
+## Installation
+
+1. Clone the repository:
+ ```bash
+ git clone https://github.com/gmbrax/Pilgrim.git
+ ```
+2. Navigate to the project directory:
+ ```bash
+ cd Pilgrim
+ ```
+3. Create a virtual environment and, then, activate it:
+ ```bash
+ python -m venv .venv
+ source .venv/bin/activate
+ ```
+4. Install the required dependencies:
+ ```bash
+ pip install -r requirements.txt
+ ```
+
+## Usage
+
+To run the main application, execute:
+
+```bash
+python ??>.py
+```
+
+This will start the Python_Pilgrim application. Follow the on-screen instructions to create and manage your travel diaries.
\ No newline at end of file
diff --git a/src/pilgrim/application.py b/src/pilgrim/application.py
index abf7b68..674cc0c 100644
--- a/src/pilgrim/application.py
+++ b/src/pilgrim/application.py
@@ -1,5 +1,4 @@
from pilgrim.database import Database
-from pilgrim.service.mocks.service_manager_mock import ServiceManagerMock
from pilgrim.service.servicemanager import ServiceManager
from pilgrim.ui.ui import UIApp
diff --git a/src/pilgrim/database.py b/src/pilgrim/database.py
index 389b15b..b940c54 100644
--- a/src/pilgrim/database.py
+++ b/src/pilgrim/database.py
@@ -12,10 +12,13 @@ class Database:
echo=False,
connect_args={"check_same_thread": False},
)
- self.session = sessionmaker(bind=self.engine, autoflush=False, autocommit=False)
+ self._session_maker = sessionmaker(bind=self.engine, autoflush=False, autocommit=False)
def create(self):
Base.metadata.create_all(self.engine)
+ def session(self):
+ return self._session_maker()
+
def get_db(self):
- return self.session()
+ return self._session_maker()
diff --git a/src/pilgrim/models/photo.py b/src/pilgrim/models/photo.py
index f6e0406..a164474 100644
--- a/src/pilgrim/models/photo.py
+++ b/src/pilgrim/models/photo.py
@@ -1,6 +1,8 @@
from typing import Any
+from datetime import datetime
+from pathlib import Path
-from sqlalchemy import Column, Integer, String, ForeignKey
+from sqlalchemy import Column, Integer, String, ForeignKey, DateTime
from sqlalchemy.orm import relationship
from pilgrim.models.photo_in_entry import photo_entry_association
@@ -12,7 +14,7 @@ class Photo(Base):
id = Column(Integer, primary_key=True)
filepath = Column(String)
name = Column(String)
- addition_date = Column(String)
+ addition_date = Column(DateTime, default=datetime.now)
caption = Column(String)
entries = relationship(
"Entry",
@@ -22,10 +24,16 @@ class Photo(Base):
fk_travel_diary_id = Column(Integer, ForeignKey("travel_diaries.id"),nullable=False)
- def __init__(self, filepath, name, addition_date=None, caption=None, entries=None, **kw: Any):
+ def __init__(self, filepath, name, addition_date=None, caption=None, entries=None, fk_travel_diary_id=None, **kw: Any):
super().__init__(**kw)
- self.filepath = filepath
+ # Convert Path to string if needed
+ if isinstance(filepath, Path):
+ self.filepath = str(filepath)
+ else:
+ self.filepath = filepath
self.name = name
- self.addition_date = addition_date
+ self.addition_date = addition_date if addition_date is not None else datetime.now()
self.caption = caption
- self.entries = entries
+ self.entries = entries if entries is not None else []
+ if fk_travel_diary_id is not None:
+ self.fk_travel_diary_id = fk_travel_diary_id
diff --git a/src/pilgrim/service/mocks/photo_service_mock.py b/src/pilgrim/service/mocks/photo_service_mock.py
index 1b02c56..0f9078a 100644
--- a/src/pilgrim/service/mocks/photo_service_mock.py
+++ b/src/pilgrim/service/mocks/photo_service_mock.py
@@ -12,7 +12,14 @@ class PhotoServiceMock(PhotoService):
self._next_id = 1
def create(self, filepath: Path, name: str, travel_diary_id, addition_date=None, caption=None) -> Photo | None:
- new_photo = Photo(filepath, name, addition_date=addition_date, caption=caption)
+ new_photo = Photo(
+ filepath=filepath,
+ name=name,
+ addition_date=addition_date,
+ caption=caption,
+ fk_travel_diary_id=travel_diary_id
+ )
+ new_photo.id = self._next_id
self.mock_data[self._next_id] = new_photo
self._next_id += 1
return new_photo
@@ -24,19 +31,18 @@ class PhotoServiceMock(PhotoService):
def read_all(self) -> List[Photo]:
return list(self.mock_data.values())
- def update(self, photo_id: Photo, photo_dst: Photo) -> Photo | None:
- item_to_update:Photo = self.mock_data.get(photo_id)
+ def update(self, photo_src: Photo, photo_dst: Photo) -> Photo | None:
+ item_to_update: Photo = self.mock_data.get(photo_src.id)
if item_to_update:
item_to_update.filepath = photo_dst.filepath if photo_dst.filepath else item_to_update.filepath
item_to_update.name = photo_dst.name if photo_dst.name else item_to_update.name
item_to_update.caption = photo_dst.caption if photo_dst.caption else item_to_update.caption
- item_to_update.addition_date = photo_dst.addition_date if photo_dst.addition_date\
- else item_to_update.addition_date
- item_to_update.fk_travel_diary_id = photo_dst.fk_travel_diary_id if photo_dst.fk_travel_diary_id \
- else item_to_update.fk_travel_diary_id
- item_to_update.entries.extend(photo_dst.entries)
+ item_to_update.addition_date = photo_dst.addition_date if photo_dst.addition_date else item_to_update.addition_date
+ item_to_update.fk_travel_diary_id = photo_dst.fk_travel_diary_id if photo_dst.fk_travel_diary_id else item_to_update.fk_travel_diary_id
+ if photo_dst.entries:
+ item_to_update.entries = photo_dst.entries
return item_to_update
return None
- def delete(self, photo_id: int) -> Photo | None:
- return self.mock_data.pop(photo_id, None)
+ def delete(self, photo_src: Photo) -> Photo | None:
+ return self.mock_data.pop(photo_src.id, None)
diff --git a/src/pilgrim/service/photo_service.py b/src/pilgrim/service/photo_service.py
index ef7c0c6..df87902 100644
--- a/src/pilgrim/service/photo_service.py
+++ b/src/pilgrim/service/photo_service.py
@@ -1,5 +1,6 @@
from pathlib import Path
from typing import List
+from datetime import datetime
from pilgrim.models.photo import Photo
@@ -9,11 +10,25 @@ class PhotoService:
def __init__(self, session):
self.session = session
- def create(self, filepath:Path, name:str, travel_diary_id, addition_date=None, caption=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()
if not travel_diary:
return None
- new_photo = Photo(filepath, name, addition_date=addition_date, caption=caption)
+
+ # Convert addition_date string to datetime if needed
+ if isinstance(addition_date, str):
+ try:
+ addition_date = datetime.strptime(addition_date, "%Y-%m-%d %H:%M:%S")
+ except ValueError:
+ addition_date = None
+
+ new_photo = Photo(
+ filepath=filepath,
+ name=name,
+ caption=caption,
+ fk_travel_diary_id=travel_diary_id,
+ addition_date=addition_date
+ )
self.session.add(new_photo)
self.session.commit()
self.session.refresh(new_photo)
@@ -25,24 +40,37 @@ class PhotoService:
def read_all(self) -> List[Photo]:
return self.session.query(Photo).all()
- def update(self,photo_src:Photo,photo_dst:Photo) -> Photo | None:
- original:Photo = self.read_by_id(photo_src.id)
+ def update(self, photo_src: Photo, photo_dst: Photo) -> Photo | None:
+ original: Photo = self.read_by_id(photo_src.id)
if original:
original.filepath = photo_dst.filepath
original.name = photo_dst.name
original.addition_date = photo_dst.addition_date
original.caption = photo_dst.caption
- original.entries.extend(photo_dst.entries)
+ if photo_dst.entries and len(photo_dst.entries) > 0:
+ if original.entries is None:
+ original.entries = []
+ original.entries = photo_dst.entries # Replace instead of extend
self.session.commit()
self.session.refresh(original)
return original
return None
- def delete(self, photo_src:Photo) -> Photo | None:
+ def delete(self, photo_src: Photo) -> Photo | None:
excluded = self.read_by_id(photo_src.id)
if excluded:
+ # Store photo data before deletion
+ deleted_photo = Photo(
+ filepath=excluded.filepath,
+ name=excluded.name,
+ addition_date=excluded.addition_date,
+ caption=excluded.caption,
+ fk_travel_diary_id=excluded.fk_travel_diary_id,
+ id=excluded.id
+ )
+
self.session.delete(excluded)
self.session.commit()
- self.session.refresh(excluded)
- return excluded
+
+ return deleted_photo
return None
diff --git a/src/pilgrim/service/servicemanager.py b/src/pilgrim/service/servicemanager.py
index b0b0cde..b3ef9d0 100644
--- a/src/pilgrim/service/servicemanager.py
+++ b/src/pilgrim/service/servicemanager.py
@@ -1,4 +1,5 @@
from pilgrim.service.entry_service import EntryService
+from pilgrim.service.photo_service import PhotoService
from pilgrim.service.travel_diary_service import TravelDiaryService
@@ -17,3 +18,7 @@ class ServiceManager:
if self.session is not None:
return TravelDiaryService(self.session)
return None
+ def get_photo_service(self):
+ if self.session is not None:
+ return PhotoService(self.session)
+ return None
\ No newline at end of file
diff --git a/src/pilgrim/ui/screens/edit_entry_screen.py b/src/pilgrim/ui/screens/edit_entry_screen.py
index a38b8df..1408ccd 100644
--- a/src/pilgrim/ui/screens/edit_entry_screen.py
+++ b/src/pilgrim/ui/screens/edit_entry_screen.py
@@ -1,15 +1,21 @@
from typing import Optional, List
import asyncio
from datetime import datetime
+from pathlib import Path
from textual.app import ComposeResult
from textual.screen import Screen
-from textual.widgets import Header, Footer, Static, TextArea
+from textual.widgets import Header, Footer, Static, TextArea, OptionList, Input, Button
from textual.binding import Binding
-from textual.containers import Container, Horizontal
+from textual.containers import Container, Horizontal, Vertical, ScrollableContainer
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
@@ -21,13 +27,16 @@ class EditEntryScreen(Screen):
Binding("ctrl+n", "next_entry", "Next/New Entry"),
Binding("ctrl+b", "prev_entry", "Previous Entry"),
Binding("ctrl+r", "rename_entry", "Rename Entry"),
- Binding("escape", "back_to_list", "Back to List")
+ Binding("escape", "back_to_list", "Back to List"),
+ Binding("f8", "toggle_sidebar", "Toggle Sidebar"),
+ Binding("f9", "toggle_focus", "Focus Sidebar/Editor"),
]
def __init__(self, diary_id: int = 1):
+ print("DEBUG: EditEntryScreen INIT")
super().__init__()
self.diary_id = diary_id
- self.diary_name = f"Diary {diary_id}" # Use a better default name
+ self.diary_name = f"Diary {diary_id}"
self.current_entry_index = 0
self.entries: List[Entry] = []
self.is_new_entry = False
@@ -38,6 +47,9 @@ class EditEntryScreen(Screen):
self._updating_display = False
self._original_content = ""
self.is_refreshing = False
+ self.sidebar_visible = False
+ self.sidebar_focused = False
+ self._sidebar_opened_once = False
# Main header
self.header = Header(name="Pilgrim v6", classes="EditEntryScreen-header")
@@ -60,6 +72,27 @@ class EditEntryScreen(Screen):
# Text area
self.text_entry = TextArea(id="text_entry", classes="EditEntryScreen-text-entry")
+ # Sidebar widgets
+ self.sidebar_title = Static("📸 Photos", classes="EditEntryScreen-sidebar-title")
+ self.photo_list = OptionList(id="photo_list", classes="EditEntryScreen-sidebar-photo-list")
+ self.photo_info = Static("", classes="EditEntryScreen-sidebar-photo-info")
+ self.help_text = Static("", classes="EditEntryScreen-sidebar-help")
+
+ # Sidebar container: photo list and info in a flexible container, help_text fixed at bottom
+ self.sidebar_content = Vertical(
+ self.photo_list,
+ self.photo_info,
+ id="sidebar_content",
+ classes="EditEntryScreen-sidebar-content"
+ )
+ self.sidebar = Vertical(
+ self.sidebar_title,
+ self.sidebar_content,
+ self.help_text, # Always at the bottom, never scrolls
+ id="sidebar",
+ classes="EditEntryScreen-sidebar"
+ )
+
# Main container
self.main = Container(
self.sub_header,
@@ -71,16 +104,32 @@ class EditEntryScreen(Screen):
# Footer
self.footer = Footer(classes="EditEntryScreen-footer")
+ def _update_footer_context(self):
+ """Forces footer refresh to show updated bindings"""
+ self.refresh()
+
def compose(self) -> ComposeResult:
+ print("DEBUG: EditEntryScreen COMPOSE", getattr(self, 'sidebar_visible', None))
yield self.header
- yield self.main
+ yield Horizontal(
+ self.main,
+ self.sidebar,
+ id="content_container",
+ classes="EditEntryScreen-content-container"
+ )
yield self.footer
def on_mount(self) -> None:
"""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()
def update_diary_info(self):
"""Updates diary information"""
@@ -93,17 +142,14 @@ class EditEntryScreen(Screen):
self.diary_name = diary.name
self.diary_info.update(f"Diary: {self.diary_name}")
else:
- # If diary not found, try to get a default name
self.diary_name = f"Diary {self.diary_id}"
self.diary_info.update(f"Diary: {self.diary_name}")
self.notify(f"Diary {self.diary_id} not found, using default name")
except Exception as e:
- # If there's an error, use a default name but don't break the app
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)}")
- # Always ensure the diary info is updated
self._ensure_diary_info_updated()
def _ensure_diary_info_updated(self):
@@ -111,7 +157,6 @@ class EditEntryScreen(Screen):
try:
self.diary_info.update(f"Diary: {self.diary_name}")
except Exception as e:
- # If even this fails, at least try to show something
self.diary_info.update(f"Diary: {self.diary_id}")
def refresh_entries(self):
@@ -120,14 +165,10 @@ class EditEntryScreen(Screen):
service_manager = self.app.service_manager
entry_service = service_manager.get_entry_service()
- # Get all entries for this diary
all_entries = entry_service.read_all()
self.entries = [entry for entry in all_entries if entry.fk_travel_diary_id == self.diary_id]
-
- # Sort by ID
self.entries.sort(key=lambda x: x.id)
- # Update next entry ID
if self.entries:
self.next_entry_id = max(entry.id for entry in self.entries) + 1
else:
@@ -139,41 +180,8 @@ class EditEntryScreen(Screen):
except Exception as e:
self.notify(f"Error loading entries: {str(e)}")
- # Ensure diary info is updated even if entries fail to load
self._ensure_diary_info_updated()
- async def async_refresh_entries(self):
- """Asynchronous version of refresh"""
- if self.is_refreshing:
- return
-
- self.is_refreshing = True
-
- try:
- service_manager = self.app.service_manager
- entry_service = service_manager.get_entry_service()
-
- # For now, use synchronous method since mock doesn't have async
- all_entries = entry_service.read_all()
- self.entries = [entry for entry in all_entries if entry.fk_travel_diary_id == self.diary_id]
-
- # Sort by ID
- self.entries.sort(key=lambda x: x.id)
-
- # Update next entry ID
- if self.entries:
- self.next_entry_id = max(entry.id for entry in self.entries) + 1
- else:
- self.next_entry_id = 1
-
- self._update_entry_display()
- self._update_sub_header()
-
- except Exception as e:
- self.notify(f"Error loading entries: {str(e)}")
- finally:
- self.is_refreshing = False
-
def _update_status_indicator(self, text: str, css_class: str):
"""Helper to update status indicator text and class."""
self.status_indicator.update(text)
@@ -211,6 +219,8 @@ class EditEntryScreen(Screen):
"""Finishes the display update by reactivating change detection"""
self._updating_display = False
self._update_sub_header()
+ if self.sidebar_visible:
+ self._update_sidebar_content()
def _update_entry_display(self):
"""Updates the display of the current entry"""
@@ -237,6 +247,342 @@ class EditEntryScreen(Screen):
self.call_after_refresh(self._finish_display_update)
+ def _update_sidebar_content(self):
+ """Updates the sidebar content with photos for the current diary"""
+ photos = self._load_photos_for_diary(self.diary_id)
+
+ # Clear existing options safely
+ self.photo_list.clear_options()
+
+ # Add 'Ingest Photo' option at the top
+ self.photo_list.add_option("➕ Ingest Photo")
+
+ if not photos:
+ self.photo_info.update("No photos found for this diary")
+ self.help_text.update("📸 No photos available\n\nUse Photo Manager to add photos")
+ return
+
+ # Add photos to the list
+ for photo in photos:
+ self.photo_list.add_option(f"📷 {photo.name}")
+
+ self.photo_info.update(f"📸 {len(photos)} photos in diary")
+
+ # English, visually distinct help text
+ help_text = (
+ "[b]⌨️ Sidebar Shortcuts[/b]\n"
+ "[b][green]i[/green][/b]: Insert photo into entry\n"
+ "[b][green]n[/green][/b]: Add new photo\n"
+ "[b][green]d[/green][/b]: Delete selected photo\n"
+ "[b][green]e[/green][/b]: Edit selected photo\n"
+ "[b][yellow]Tab[/yellow][/b]: Back to editor\n"
+ "[b][yellow]F8[/yellow][/b]: Show/hide sidebar\n"
+ "[b][yellow]F9[/yellow][/b]: Switch focus (if needed)"
+ )
+ self.help_text.update(help_text)
+
+ 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()
+ 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"""
+ print("DEBUG: TOGGLE SIDEBAR", self.sidebar_visible)
+ self.sidebar_visible = not self.sidebar_visible
+
+ if self.sidebar_visible:
+ self.sidebar.display = True
+ self._update_sidebar_content()
+ # Automatically focus the sidebar when opening
+ self.sidebar_focused = True
+ self.photo_list.focus()
+ # Notification when opening the sidebar for the first time
+ if not self._sidebar_opened_once:
+ self.notify(
+ "Sidebar opened and focused! Use the shortcuts shown in the help panel.",
+ severity="info"
+ )
+ self._sidebar_opened_once = True
+ else:
+ self.sidebar.display = False
+ self.sidebar_focused = False # Reset focus when hiding
+ self.text_entry.focus() # Return focus to editor
+
+ # Update footer after context change
+ self._update_footer_context()
+ self.refresh(layout=True)
+
+ def action_toggle_focus(self):
+ """Toggles focus between editor and sidebar"""
+ print("DEBUG: TOGGLE FOCUS", self.sidebar_visible, self.sidebar_focused)
+ if not self.sidebar_visible:
+ # If sidebar is not visible, show it and focus it
+ self.action_toggle_sidebar()
+ return
+
+ self.sidebar_focused = not self.sidebar_focused
+ if self.sidebar_focused:
+ self.photo_list.focus()
+ else:
+ self.text_entry.focus()
+
+ # Update footer after focus change
+ self._update_footer_context()
+
+ def action_insert_photo(self):
+ """Insert selected photo into text"""
+ if not self.sidebar_focused or not self.sidebar_visible:
+ self.notify("Use F8 to open the sidebar first.", severity="warning")
+ return
+
+ # Get selected photo
+ 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]
+
+ # Insert photo reference into text
+ photo_ref = f"\n[📷 {selected_photo.name}]({selected_photo.filepath})\n"
+ if selected_photo.caption:
+ photo_ref += f"*{selected_photo.caption}*\n"
+
+ # Insert at cursor position or at end
+ current_text = self.text_entry.text
+ cursor_position = len(current_text) # Insert at end for now
+ new_text = current_text + photo_ref
+ self.text_entry.text = new_text
+
+ self.notify(f"Inserted photo: {selected_photo.name}")
+
+ def action_ingest_new_photo(self):
+ """Ingest a new photo using modal"""
+ if not self.sidebar_focused or not self.sidebar_visible:
+ self.notify("Use F8 to open the sidebar first.", severity="warning")
+ return
+
+ # Open add photo modal
+ self.app.push_screen(
+ AddPhotoModal(diary_id=self.diary_id),
+ self.handle_add_photo_result
+ )
+
+ def handle_add_photo_result(self, result: dict | None) -> None:
+ """Callback that processes the add photo modal result."""
+ if result is None:
+ self.notify("Add photo cancelled")
+ return
+
+ # 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"""
+ try:
+ service_manager = self.app.service_manager
+ 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"],
+ travel_diary_id=self.diary_id,
+ addition_date=current_date,
+ caption=photo_data["caption"]
+ )
+
+ if new_photo:
+ self.notify(f"Photo '{new_photo.name}' added successfully!")
+ # Refresh sidebar content
+ if self.sidebar_visible:
+ self._update_sidebar_content()
+ else:
+ self.notify("Error creating photo")
+
+ except Exception as e:
+ self.notify(f"Error creating photo: {str(e)}")
+
+ def action_delete_photo(self):
+ """Delete selected photo"""
+ 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),
+ 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"""
+ try:
+ service_manager = self.app.service_manager
+ photo_service = service_manager.get_photo_service()
+
+ result = photo_service.delete(photo)
+
+ if result:
+ self.notify(f"Photo '{photo.name}' deleted successfully!")
+ # Refresh sidebar content
+ if self.sidebar_visible:
+ self._update_sidebar_content()
+ else:
+ self.notify("Error deleting photo")
+
+ except Exception as e:
+ self.notify(f"Error deleting photo: {str(e)}")
+
+ def action_edit_photo(self):
+ """Edit selected photo using modal"""
+ if not self.sidebar_focused or not self.sidebar_visible:
+ self.notify("Use F8 to open the sidebar first.", severity="warning")
+ return
+
+ 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),
+ self.handle_edit_photo_result
+ )
+
+ def handle_edit_photo_result(self, result: dict | None) -> None:
+ """Callback that processes the edit photo modal result."""
+ if result is None:
+ self.notify("Edit photo cancelled")
+ return
+
+ # 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)
+
+ async def _async_update_photo(self, original_photo: Photo, photo_data: dict):
+ """Updates a photo asynchronously"""
+ try:
+ service_manager = self.app.service_manager
+ photo_service = service_manager.get_photo_service()
+
+ # Create updated photo object
+ updated_photo = Photo(
+ filepath=photo_data["filepath"],
+ name=photo_data["name"],
+ addition_date=original_photo.addition_date,
+ caption=photo_data["caption"],
+ entries=original_photo.entries if original_photo.entries is not None else [],
+ id=original_photo.id
+ )
+
+ result = photo_service.update(original_photo, updated_photo)
+
+ if result:
+ self.notify(f"Photo '{updated_photo.name}' updated successfully!")
+ # Refresh sidebar content
+ if self.sidebar_visible:
+ self._update_sidebar_content()
+ else:
+ self.notify("Error updating photo")
+
+ except Exception as e:
+ self.notify(f"Error updating photo: {str(e)}")
+
+ def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None:
+ """Handles photo selection in the sidebar"""
+ if not self.sidebar_visible:
+ return
+ # If 'Ingest Photo' is selected (always index 0)
+ if event.option_index == 0:
+ self.action_ingest_new_photo()
+ return
+ photos = self._load_photos_for_diary(self.diary_id)
+ # Adjust index because of 'Ingest Photo' at the top
+ photo_index = event.option_index - 1
+ if not photos or photo_index >= len(photos):
+ return
+ selected_photo = photos[photo_index]
+ self.notify(f"Selected photo: {selected_photo.name}")
+ # Update photo info with details
+ photo_details = f"📷 {selected_photo.name}\n"
+ photo_details += f"📅 {selected_photo.addition_date}\n"
+ if selected_photo.caption:
+ photo_details += f"💬 {selected_photo.caption}\n"
+ photo_details += f"📁 {selected_photo.filepath}"
+ self.photo_info.update(photo_details)
+
def on_text_area_changed(self, event) -> None:
"""Detects text changes to mark as unsaved"""
if (hasattr(self, 'text_entry') and not self.text_entry.read_only and
@@ -251,6 +597,17 @@ class EditEntryScreen(Screen):
self.has_unsaved_changes = False
self._update_sub_header()
+ def on_focus(self, event) -> None:
+ """Captures focus changes to update footer"""
+ # Check if focus changed to/from sidebar
+ if hasattr(event.widget, 'id'):
+ if event.widget.id == "photo_list":
+ self.sidebar_focused = True
+ self._update_footer_context()
+ elif event.widget.id == "text_entry":
+ self.sidebar_focused = False
+ self._update_footer_context()
+
def action_back_to_list(self) -> None:
"""Goes back to the diary list"""
if self.is_new_entry and not self.text_entry.text.strip() and not self.has_unsaved_changes:
@@ -375,7 +732,6 @@ class EditEntryScreen(Screen):
service_manager = self.app.service_manager
entry_service = service_manager.get_entry_service()
- # Get current date as datetime object
current_date = datetime.now()
new_entry = entry_service.create(
@@ -389,7 +745,6 @@ class EditEntryScreen(Screen):
self.entries.append(new_entry)
self.entries.sort(key=lambda x: x.id)
- # Find the new entry index
for i, entry in enumerate(self.entries):
if entry.id == new_entry.id:
self.current_entry_index = i
@@ -419,7 +774,6 @@ class EditEntryScreen(Screen):
current_entry = self.entries[self.current_entry_index]
updated_content = self.text_entry.text
- # Create updated entry object
updated_entry = Entry(
title=current_entry.title,
text=updated_content,
@@ -445,8 +799,44 @@ class EditEntryScreen(Screen):
except Exception as e:
self.notify(f"Error updating entry: {str(e)}")
- def action_force_refresh(self):
- """Forces manual refresh"""
- self.notify("Forcing refresh...")
- self.refresh_entries()
- self.call_later(self.async_refresh_entries)
\ No newline at end of file
+ def on_key(self, event):
+ # Sidebar contextual shortcuts
+ if self.sidebar_focused and self.sidebar_visible:
+ if event.key == "i":
+ self.action_insert_photo()
+ event.stop()
+ elif event.key == "n":
+ self.action_ingest_new_photo()
+ event.stop()
+ elif event.key == "d":
+ self.action_delete_photo()
+ event.stop()
+ elif event.key == "e":
+ self.action_edit_photo()
+ event.stop()
+ # Shift+Tab: remove indent
+ elif self.focused is self.text_entry and event.key == "shift+tab":
+ textarea = self.text_entry
+ row, col = textarea.cursor_location
+ lines = textarea.text.splitlines()
+ if row < len(lines):
+ line = lines[row]
+ if line.startswith('\t'):
+ lines[row] = line[1:]
+ textarea.text = '\n'.join(lines)
+ textarea.cursor_location = (row, max(col - 1, 0))
+ elif line.startswith(' '): # 4 spaces
+ lines[row] = line[4:]
+ textarea.text = '\n'.join(lines)
+ textarea.cursor_location = (row, max(col - 4, 0))
+ elif line.startswith(' '):
+ n = len(line) - len(line.lstrip(' '))
+ to_remove = min(n, 4)
+ lines[row] = line[to_remove:]
+ textarea.text = '\n'.join(lines)
+ textarea.cursor_location = (row, max(col - to_remove, 0))
+ event.stop()
+ # Tab: insert tab
+ elif self.focused is self.text_entry and event.key == "tab":
+ self.text_entry.insert('\t')
+ event.stop()
\ No newline at end of file
diff --git a/src/pilgrim/ui/screens/modals/add_photo_modal.py b/src/pilgrim/ui/screens/modals/add_photo_modal.py
new file mode 100644
index 0000000..e0ac578
--- /dev/null
+++ b/src/pilgrim/ui/screens/modals/add_photo_modal.py
@@ -0,0 +1,105 @@
+import os
+from pathlib import Path
+from textual.app import ComposeResult
+from textual.screen import Screen
+from textual.widgets import Static, Input, Button
+from textual.containers import Horizontal, Container
+from .file_picker_modal import FilePickerModal
+
+class AddPhotoModal(Screen):
+ """Modal for adding a new photo"""
+ def __init__(self, diary_id: int):
+ super().__init__()
+ self.diary_id = diary_id
+ self.result = None
+
+ def compose(self) -> ComposeResult:
+ yield Container(
+ Static("📷 Add New Photo", classes="AddPhotoModal-Title"),
+ Static("File path:", classes="AddPhotoModal-Label"),
+ Horizontal(
+ Input(placeholder="Enter file path...", id="filepath-input", classes="AddPhotoModal-Input"),
+ Button("Escolher arquivo...", id="choose-file-button", classes="AddPhotoModal-Button"),
+ classes="AddPhotoModal-FileRow"
+ ),
+ Static("Photo name:", classes="AddPhotoModal-Label"),
+ Input(placeholder="Enter photo name...", id="name-input", classes="AddPhotoModal-Input"),
+ Static("Caption (optional):", classes="AddPhotoModal-Label"),
+ Input(placeholder="Enter caption...", id="caption-input", classes="AddPhotoModal-Input"),
+ Horizontal(
+ Button("Add Photo", id="add-button", classes="AddPhotoModal-Button"),
+ Button("Cancel", id="cancel-button", classes="AddPhotoModal-Button"),
+ classes="AddPhotoModal-Buttons"
+ ),
+ classes="AddPhotoModal-Dialog"
+ )
+
+ def on_button_pressed(self, event: Button.Pressed) -> None:
+ if event.button.id == "choose-file-button":
+ self.app.push_screen(
+ FilePickerModal(),
+ self.handle_file_picker_result
+ )
+ return
+ if event.button.id == "add-button":
+ filepath = self.query_one("#filepath-input", Input).value
+ name = self.query_one("#name-input", Input).value
+ caption = self.query_one("#caption-input", Input).value
+ if not filepath.strip() or not name.strip():
+ self.notify("File path and name are required", severity="error")
+ return
+
+ # 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
+ })
+ 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:
+ # 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")
\ No newline at end of file
diff --git a/src/pilgrim/ui/screens/modals/confirm_delete_modal.py b/src/pilgrim/ui/screens/modals/confirm_delete_modal.py
new file mode 100644
index 0000000..b987de4
--- /dev/null
+++ b/src/pilgrim/ui/screens/modals/confirm_delete_modal.py
@@ -0,0 +1,32 @@
+from textual.app import ComposeResult
+from textual.screen import Screen
+from textual.widgets import Static, Button
+from textual.containers import Container, Horizontal
+from pilgrim.models.photo import Photo
+
+class ConfirmDeleteModal(Screen):
+ """Modal for confirming photo deletion"""
+ def __init__(self, photo: Photo):
+ super().__init__()
+ self.photo = photo
+ self.result = None
+
+ def compose(self) -> ComposeResult:
+ yield Container(
+ Static("🗑️ Confirm Deletion", classes="ConfirmDeleteModal-Title"),
+ Static(f"Are you sure you want to delete the photo '{self.photo.name}'?", classes="ConfirmDeleteModal-Message"),
+ Static("This action cannot be undone.", classes="ConfirmDeleteModal-Warning"),
+ Horizontal(
+ Button("Delete", variant="error", id="delete-button", classes="ConfirmDeleteModal-Button"),
+ Button("Cancel", variant="default", id="cancel-button", classes="ConfirmDeleteModal-Button"),
+ classes="ConfirmDeleteModal-Buttons"
+ ),
+ classes="ConfirmDeleteModal-Dialog"
+ )
+
+ def on_button_pressed(self, event: Button.Pressed) -> None:
+ if event.button.id == "delete-button":
+ self.result = True
+ self.dismiss(True)
+ elif event.button.id == "cancel-button":
+ self.dismiss(False)
\ No newline at end of file
diff --git a/src/pilgrim/ui/screens/modals/edit_photo_modal.py b/src/pilgrim/ui/screens/modals/edit_photo_modal.py
new file mode 100644
index 0000000..5af3011
--- /dev/null
+++ b/src/pilgrim/ui/screens/modals/edit_photo_modal.py
@@ -0,0 +1,68 @@
+from textual.app import ComposeResult
+from textual.screen import Screen
+from textual.widgets import Static, Input, Button
+from textual.containers import Container, Horizontal
+from pilgrim.models.photo import Photo
+
+class EditPhotoModal(Screen):
+ """Modal for editing an existing photo (name and caption only)"""
+ def __init__(self, photo: Photo):
+ super().__init__()
+ self.photo = photo
+ self.result = None
+
+ def compose(self) -> ComposeResult:
+ yield Container(
+ Static("✏️ Edit Photo", classes="EditPhotoModal-Title"),
+ Static("File path (read-only):", classes="EditPhotoModal-Label"),
+ Input(
+ value=self.photo.filepath,
+ id="filepath-input",
+ classes="EditPhotoModal-Input",
+ disabled=True
+ ),
+ Static("Photo name:", classes="EditPhotoModal-Label"),
+ Input(
+ value=self.photo.name,
+ placeholder="Enter photo name...",
+ id="name-input",
+ classes="EditPhotoModal-Input"
+ ),
+ Static("Caption (optional):", classes="EditPhotoModal-Label"),
+ Input(
+ value=self.photo.caption or "",
+ placeholder="Enter caption...",
+ id="caption-input",
+ classes="EditPhotoModal-Input"
+ ),
+ Horizontal(
+ Button("Save Changes", id="save-button", classes="EditPhotoModal-Button"),
+ Button("Cancel", id="cancel-button", classes="EditPhotoModal-Button"),
+ classes="EditPhotoModal-Buttons"
+ ),
+ classes="EditPhotoModal-Dialog"
+ )
+
+ def on_button_pressed(self, event: Button.Pressed) -> None:
+ if event.button.id == "save-button":
+ name = self.query_one("#name-input", Input).value
+ caption = self.query_one("#caption-input", Input).value
+
+ if not name.strip():
+ self.notify("Photo name is required", severity="error")
+ return
+
+ # Return the updated photo data
+ self.result = {
+ "filepath": self.photo.filepath, # Keep original filepath
+ "name": name.strip(),
+ "caption": caption.strip() if caption.strip() else None
+ }
+ self.dismiss(self.result)
+
+ elif event.button.id == "cancel-button":
+ self.dismiss()
+
+ def on_mount(self) -> None:
+ """Focus on the name input when modal opens"""
+ self.query_one("#name-input", Input).focus()
\ No newline at end of file
diff --git a/src/pilgrim/ui/screens/modals/file_picker_modal.py b/src/pilgrim/ui/screens/modals/file_picker_modal.py
new file mode 100644
index 0000000..c65f206
--- /dev/null
+++ b/src/pilgrim/ui/screens/modals/file_picker_modal.py
@@ -0,0 +1,66 @@
+import os
+from pathlib import Path
+from typing import Iterable
+from textual.app import ComposeResult
+from textual.screen import Screen
+from textual.widgets import Static, DirectoryTree, Button
+from textual.containers import Horizontal, Container
+
+class ImageDirectoryTree(DirectoryTree):
+ """DirectoryTree that only shows image files"""
+
+ def filter_paths(self, paths: Iterable[Path]) -> Iterable[Path]:
+ """Filter to show only directories and image files"""
+ image_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'}
+ return [
+ path for path in paths
+ if path.is_dir() or path.suffix.lower() in image_extensions
+ ]
+
+class FilePickerModal(Screen):
+ """Modal for picking an image file using DirectoryTree"""
+
+ def __init__(self, start_path=None):
+ super().__init__()
+ self.start_path = Path(start_path or os.getcwd())
+ # Start one level up to make navigation easier
+ self.current_path = self.start_path.parent
+
+ def compose(self) -> ComposeResult:
+ yield Container(
+ Static(f"Current: {self.current_path}", id="title", classes="FilePickerModal-Title"),
+ ImageDirectoryTree(str(self.current_path), id="directory-tree"),
+ Horizontal(
+ Button("Up", id="up-button", classes="FilePickerModal-Button"),
+ Button("Cancel", id="cancel-button", classes="FilePickerModal-Button"),
+ classes="FilePickerModal-Buttons"
+ ),
+ classes="FilePickerModal-Dialog"
+ )
+
+ def on_directory_tree_file_selected(self, event: DirectoryTree.FileSelected) -> None:
+ """Handle file selection"""
+ file_path = event.path
+ # Check if it's an image file
+ image_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'}
+ if file_path.suffix.lower() in image_extensions:
+ # Return the file path as result
+ self.dismiss(str(file_path))
+ else:
+ self.notify("Please select an image file", severity="warning")
+
+ def on_button_pressed(self, event: Button.Pressed) -> None:
+ """Handle button presses"""
+ if event.button.id == "up-button":
+ # Navigate to parent directory
+ parent = self.current_path.parent
+ if parent != self.current_path:
+ self.current_path = parent
+ self.query_one("#title", Static).update(f"Current: {self.current_path}")
+ # Reload the directory tree
+ tree = self.query_one("#directory-tree", ImageDirectoryTree)
+ tree.path = str(self.current_path)
+ tree.reload()
+ elif event.button.id == "cancel-button":
+ # Return None to indicate cancellation
+ self.dismiss(None)
\ No newline at end of file
diff --git a/src/pilgrim/ui/styles/pilgrim.css b/src/pilgrim/ui/styles/pilgrim.css
index e170af2..5b52192 100644
--- a/src/pilgrim/ui/styles/pilgrim.css
+++ b/src/pilgrim/ui/styles/pilgrim.css
@@ -387,4 +387,243 @@ Screen.-modal {
.RenameEntryModal-cancel-button {
margin: 0 1;
width: 1fr;
+}
+
+.EditEntryScreen-sidebar {
+ width: 40;
+ min-height: 10;
+ border-left: solid green;
+ padding: 1;
+ background: $surface-darken-2;
+ color: $primary;
+ text-style: bold;
+ content-align: left top;
+}
+
+.EditEntryScreen-content-container {
+ layout: horizontal;
+ height: 1fr;
+}
+
+.EditEntryScreen-sidebar-title {
+ text-align: center;
+ text-style: bold;
+ color: $accent;
+ padding: 1;
+ border-bottom: solid $accent;
+ margin-bottom: 1;
+}
+
+.EditEntryScreen-sidebar-content {
+ height: 1fr;
+ layout: vertical;
+}
+
+.EditEntryScreen-sidebar-photo-list {
+ height: 1fr;
+ border: solid $accent;
+ margin-bottom: 1;
+}
+
+.EditEntryScreen-sidebar-photo-info {
+ height: auto;
+ min-height: 3;
+ border: solid $warning;
+ padding: 1;
+ margin-bottom: 1;
+ background: $surface-darken-1;
+}
+
+.EditEntryScreen-sidebar-help {
+ height: auto;
+ min-height: 8;
+ border: solid $success;
+ padding: 1;
+ background: $surface-darken-1;
+ text-style: italic;
+}
+
+/* Photo Modal Styles */
+.modal-dialog {
+ layout: vertical;
+ width: 60%;
+ height: auto;
+ background: $surface;
+ border: thick $accent;
+ padding: 2 4;
+ align: center middle;
+}
+
+.modal-title {
+ text-align: center;
+ text-style: bold;
+ color: $primary;
+ margin-bottom: 1;
+}
+
+.modal-label {
+ margin-bottom: 1;
+ color: $text;
+}
+
+.modal-input {
+ width: 1fr;
+ margin-bottom: 2;
+}
+
+.modal-buttons {
+ width: 1fr;
+ height: auto;
+ align: center middle;
+ padding-top: 1;
+}
+
+.modal-button {
+ margin: 0 1;
+ width: 1fr;
+}
+
+/* AddPhotoModal styles */
+.AddPhotoModal-Dialog {
+ layout: vertical;
+ width: 60%;
+ height: auto;
+ background: $surface;
+ border: thick $accent;
+ padding: 2 4;
+ align: center middle;
+}
+.AddPhotoModal-Title {
+ text-align: center;
+ text-style: bold;
+ color: $primary;
+ margin-bottom: 1;
+}
+.AddPhotoModal-Label {
+ margin-bottom: 1;
+ color: $text;
+}
+.AddPhotoModal-Input {
+ width: 1fr;
+ margin-bottom: 2;
+}
+.AddPhotoModal-Buttons {
+ width: 1fr;
+ height: auto;
+ align: center middle;
+ padding-top: 1;
+}
+.AddPhotoModal-Button {
+ margin: 0 1;
+ width: 1fr;
+}
+
+/* EditPhotoModal styles */
+.EditPhotoModal-Dialog {
+ layout: vertical;
+ width: 60%;
+ height: auto;
+ background: $surface;
+ border: thick $accent;
+ padding: 2 4;
+ align: center middle;
+}
+.EditPhotoModal-Title {
+ text-align: center;
+ text-style: bold;
+ color: $primary;
+ margin-bottom: 1;
+}
+.EditPhotoModal-Label {
+ margin-bottom: 1;
+ color: $text;
+}
+.EditPhotoModal-Input {
+ width: 1fr;
+ margin-bottom: 2;
+}
+.EditPhotoModal-Buttons {
+ width: 1fr;
+ height: auto;
+ align: center middle;
+ padding-top: 1;
+}
+.EditPhotoModal-Button {
+ margin: 0 1;
+ width: 1fr;
+}
+
+/* FilePickerModal styles */
+.FilePickerModal-Dialog {
+ layout: vertical;
+ width: 80%;
+ height: 80%;
+ background: $surface;
+ border: thick $accent;
+ padding: 2 4;
+ align: center middle;
+}
+
+.FilePickerModal-Title {
+ text-align: center;
+ text-style: bold;
+ color: $primary;
+ margin-bottom: 1;
+}
+
+.FilePickerModal-Buttons {
+ width: 1fr;
+ height: auto;
+ align: center middle;
+ padding-top: 1;
+}
+
+.FilePickerModal-Button {
+ margin: 0 1;
+ width: 1fr;
+}
+
+/* DirectoryTree specific styles */
+#directory-tree {
+ 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;
}
\ No newline at end of file