mirror of https://github.com/gmbrax/Pilgrim.git
Compare commits
No commits in common. "cf56fda3dea20204333ffb4b984603bf0e353294" and "84b03972637d4a46a2b1ce38d5e968cb9adb7d5c" have entirely different histories.
cf56fda3de
...
84b0397263
|
|
@ -1,6 +1,3 @@
|
||||||
database.db
|
database.db
|
||||||
__pycache__
|
__pycache__
|
||||||
/.idea/
|
/.idea/
|
||||||
.build-vend
|
|
||||||
.venv
|
|
||||||
dist_nuitka
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
from pilgrim.database import Database
|
from pilgrim.database import Database
|
||||||
|
from pilgrim.service.mocks.service_manager_mock import ServiceManagerMock
|
||||||
from pilgrim.service.servicemanager import ServiceManager
|
from pilgrim.service.servicemanager import ServiceManager
|
||||||
from pilgrim.ui.ui import UIApp
|
from pilgrim.ui.ui import UIApp
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,10 @@ class Database:
|
||||||
echo=False,
|
echo=False,
|
||||||
connect_args={"check_same_thread": False},
|
connect_args={"check_same_thread": False},
|
||||||
)
|
)
|
||||||
self._session_maker = sessionmaker(bind=self.engine, autoflush=False, autocommit=False)
|
self.session = sessionmaker(bind=self.engine, autoflush=False, autocommit=False)
|
||||||
|
|
||||||
def create(self):
|
def create(self):
|
||||||
Base.metadata.create_all(self.engine)
|
Base.metadata.create_all(self.engine)
|
||||||
|
|
||||||
def session(self):
|
|
||||||
return self._session_maker()
|
|
||||||
|
|
||||||
def get_db(self):
|
def get_db(self):
|
||||||
return self._session_maker()
|
return self.session()
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from datetime import datetime
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from sqlalchemy import Column, Integer, String, ForeignKey, DateTime
|
from sqlalchemy import Column, Integer, String, ForeignKey
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from pilgrim.models.photo_in_entry import photo_entry_association
|
from pilgrim.models.photo_in_entry import photo_entry_association
|
||||||
|
|
@ -14,7 +12,7 @@ class Photo(Base):
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
filepath = Column(String)
|
filepath = Column(String)
|
||||||
name = Column(String)
|
name = Column(String)
|
||||||
addition_date = Column(DateTime, default=datetime.now)
|
addition_date = Column(String)
|
||||||
caption = Column(String)
|
caption = Column(String)
|
||||||
entries = relationship(
|
entries = relationship(
|
||||||
"Entry",
|
"Entry",
|
||||||
|
|
@ -24,16 +22,10 @@ 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, addition_date=None, caption=None, entries=None, **kw: Any):
|
||||||
super().__init__(**kw)
|
super().__init__(**kw)
|
||||||
# Convert Path to string if needed
|
self.filepath = filepath
|
||||||
if isinstance(filepath, Path):
|
|
||||||
self.filepath = str(filepath)
|
|
||||||
else:
|
|
||||||
self.filepath = filepath
|
|
||||||
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
|
||||||
self.caption = caption
|
self.caption = caption
|
||||||
self.entries = entries if entries is not None else []
|
self.entries = entries
|
||||||
if fk_travel_diary_id is not None:
|
|
||||||
self.fk_travel_diary_id = fk_travel_diary_id
|
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,7 @@ class PhotoServiceMock(PhotoService):
|
||||||
self._next_id = 1
|
self._next_id = 1
|
||||||
|
|
||||||
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, addition_date=None, caption=None) -> Photo | None:
|
||||||
new_photo = Photo(
|
new_photo = Photo(filepath, name, addition_date=addition_date, caption=caption)
|
||||||
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.mock_data[self._next_id] = new_photo
|
||||||
self._next_id += 1
|
self._next_id += 1
|
||||||
return new_photo
|
return new_photo
|
||||||
|
|
@ -31,18 +24,19 @@ class PhotoServiceMock(PhotoService):
|
||||||
def read_all(self) -> List[Photo]:
|
def read_all(self) -> List[Photo]:
|
||||||
return list(self.mock_data.values())
|
return list(self.mock_data.values())
|
||||||
|
|
||||||
def update(self, photo_src: Photo, photo_dst: Photo) -> Photo | None:
|
def update(self, photo_id: Photo, photo_dst: Photo) -> Photo | None:
|
||||||
item_to_update: Photo = self.mock_data.get(photo_src.id)
|
item_to_update:Photo = self.mock_data.get(photo_id)
|
||||||
if item_to_update:
|
if item_to_update:
|
||||||
item_to_update.filepath = photo_dst.filepath if photo_dst.filepath else item_to_update.filepath
|
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.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.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.addition_date = photo_dst.addition_date if photo_dst.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
|
else item_to_update.addition_date
|
||||||
if photo_dst.entries:
|
item_to_update.fk_travel_diary_id = photo_dst.fk_travel_diary_id if photo_dst.fk_travel_diary_id \
|
||||||
item_to_update.entries = photo_dst.entries
|
else item_to_update.fk_travel_diary_id
|
||||||
|
item_to_update.entries.extend(photo_dst.entries)
|
||||||
return item_to_update
|
return item_to_update
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def delete(self, photo_src: Photo) -> Photo | None:
|
def delete(self, photo_id: int) -> Photo | None:
|
||||||
return self.mock_data.pop(photo_src.id, None)
|
return self.mock_data.pop(photo_id, None)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List
|
from typing import List
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
|
|
||||||
from pilgrim.models.photo import Photo
|
from pilgrim.models.photo import Photo
|
||||||
|
|
@ -10,25 +9,11 @@ class PhotoService:
|
||||||
def __init__(self, session):
|
def __init__(self, session):
|
||||||
self.session = session
|
self.session = session
|
||||||
|
|
||||||
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, addition_date=None, caption=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()
|
||||||
if not travel_diary:
|
if not travel_diary:
|
||||||
return None
|
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.add(new_photo)
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
self.session.refresh(new_photo)
|
self.session.refresh(new_photo)
|
||||||
|
|
@ -40,37 +25,24 @@ class PhotoService:
|
||||||
def read_all(self) -> List[Photo]:
|
def read_all(self) -> List[Photo]:
|
||||||
return self.session.query(Photo).all()
|
return self.session.query(Photo).all()
|
||||||
|
|
||||||
def update(self, photo_src: Photo, photo_dst: Photo) -> Photo | None:
|
def update(self,photo_src:Photo,photo_dst:Photo) -> Photo | None:
|
||||||
original: Photo = self.read_by_id(photo_src.id)
|
original:Photo = self.read_by_id(photo_src.id)
|
||||||
if original:
|
if original:
|
||||||
original.filepath = photo_dst.filepath
|
original.filepath = photo_dst.filepath
|
||||||
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
|
||||||
if photo_dst.entries and len(photo_dst.entries) > 0:
|
original.entries.extend(photo_dst.entries)
|
||||||
if original.entries is None:
|
|
||||||
original.entries = []
|
|
||||||
original.entries = photo_dst.entries # Replace instead of extend
|
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
self.session.refresh(original)
|
self.session.refresh(original)
|
||||||
return original
|
return original
|
||||||
return None
|
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)
|
excluded = self.read_by_id(photo_src.id)
|
||||||
if excluded:
|
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.delete(excluded)
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
|
self.session.refresh(excluded)
|
||||||
return deleted_photo
|
return excluded
|
||||||
return None
|
return None
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,6 @@ from pilgrim.models.entry import Entry
|
||||||
from pilgrim.models.travel_diary import TravelDiary
|
from pilgrim.models.travel_diary import TravelDiary
|
||||||
from pilgrim.models.photo import Photo
|
from pilgrim.models.photo import Photo
|
||||||
from pilgrim.ui.screens.modals.add_photo_modal import AddPhotoModal
|
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.modals.file_picker_modal import FilePickerModal
|
||||||
from pilgrim.ui.screens.rename_entry_modal import RenameEntryModal
|
from pilgrim.ui.screens.rename_entry_modal import RenameEntryModal
|
||||||
|
|
||||||
|
|
@ -277,7 +275,7 @@ class EditEntryScreen(Screen):
|
||||||
"[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]: Focus Sidebar/Editor"
|
||||||
)
|
)
|
||||||
self.help_text.update(help_text)
|
self.help_text.update(help_text)
|
||||||
|
|
||||||
|
|
@ -303,13 +301,10 @@ class EditEntryScreen(Screen):
|
||||||
if self.sidebar_visible:
|
if self.sidebar_visible:
|
||||||
self.sidebar.display = True
|
self.sidebar.display = True
|
||||||
self._update_sidebar_content()
|
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
|
# Notification when opening the sidebar for the first time
|
||||||
if not self._sidebar_opened_once:
|
if not self._sidebar_opened_once:
|
||||||
self.notify(
|
self.notify(
|
||||||
"Sidebar opened and focused! Use the shortcuts shown in the help panel.",
|
"Sidebar opened! Context-specific shortcuts are always visible in the sidebar help panel.",
|
||||||
severity="info"
|
severity="info"
|
||||||
)
|
)
|
||||||
self._sidebar_opened_once = True
|
self._sidebar_opened_once = True
|
||||||
|
|
@ -342,7 +337,7 @@ class EditEntryScreen(Screen):
|
||||||
def action_insert_photo(self):
|
def action_insert_photo(self):
|
||||||
"""Insert selected photo into text"""
|
"""Insert selected photo into text"""
|
||||||
if not self.sidebar_focused or not self.sidebar_visible:
|
if not self.sidebar_focused or not self.sidebar_visible:
|
||||||
self.notify("Use F8 to open the sidebar first.", severity="warning")
|
self.notify("Use F9 to focus the sidebar before using this shortcut.", severity="warning")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get selected photo
|
# Get selected photo
|
||||||
|
|
@ -350,15 +345,11 @@ class EditEntryScreen(Screen):
|
||||||
self.notify("No photo selected", severity="warning")
|
self.notify("No photo selected", severity="warning")
|
||||||
return
|
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)
|
photos = self._load_photos_for_diary(self.diary_id)
|
||||||
if photo_index < 0 or photo_index >= len(photos):
|
if self.photo_list.highlighted >= len(photos):
|
||||||
self.notify("No photo selected", severity="warning")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
selected_photo = photos[photo_index]
|
selected_photo = photos[self.photo_list.highlighted]
|
||||||
|
|
||||||
# Insert photo reference into text
|
# Insert photo reference into text
|
||||||
photo_ref = f"\n[📷 {selected_photo.name}]({selected_photo.filepath})\n"
|
photo_ref = f"\n[📷 {selected_photo.name}]({selected_photo.filepath})\n"
|
||||||
|
|
@ -376,7 +367,7 @@ class EditEntryScreen(Screen):
|
||||||
def action_ingest_new_photo(self):
|
def action_ingest_new_photo(self):
|
||||||
"""Ingest a new photo using modal"""
|
"""Ingest a new photo using modal"""
|
||||||
if not self.sidebar_focused or not self.sidebar_visible:
|
if not self.sidebar_focused or not self.sidebar_visible:
|
||||||
self.notify("Use F8 to open the sidebar first.", severity="warning")
|
self.notify("Use F9 to focus the sidebar before using this shortcut.", severity="warning")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Open add photo modal
|
# Open add photo modal
|
||||||
|
|
@ -391,10 +382,8 @@ class EditEntryScreen(Screen):
|
||||||
self.notify("Add photo cancelled")
|
self.notify("Add photo cancelled")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Photo was already created in the modal, just refresh the sidebar
|
# Schedule async creation
|
||||||
if self.sidebar_visible:
|
self.call_later(self._async_create_photo, result)
|
||||||
self._update_sidebar_content()
|
|
||||||
self.notify(f"Photo '{result['name']}' added successfully!")
|
|
||||||
|
|
||||||
async def _async_create_photo(self, photo_data: dict):
|
async def _async_create_photo(self, photo_data: dict):
|
||||||
"""Creates a new photo asynchronously"""
|
"""Creates a new photo asynchronously"""
|
||||||
|
|
@ -426,46 +415,24 @@ class EditEntryScreen(Screen):
|
||||||
def action_delete_photo(self):
|
def action_delete_photo(self):
|
||||||
"""Delete selected photo"""
|
"""Delete selected photo"""
|
||||||
if not self.sidebar_focused or not self.sidebar_visible:
|
if not self.sidebar_focused or not self.sidebar_visible:
|
||||||
self.notify("Use F8 to open the sidebar first.", severity="warning")
|
self.notify("Use F9 to focus the sidebar before using this shortcut.", severity="warning")
|
||||||
return
|
return
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
# 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)
|
photos = self._load_photos_for_diary(self.diary_id)
|
||||||
if photo_index < 0 or photo_index >= len(photos):
|
if self.photo_list.highlighted >= len(photos):
|
||||||
self.notify("No photo selected", severity="warning")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
selected_photo = photos[photo_index]
|
selected_photo = photos[self.photo_list.highlighted]
|
||||||
|
|
||||||
# Open confirm delete modal
|
# Confirm deletion
|
||||||
self.app.push_screen(
|
self.notify(f"Deleting photo: {selected_photo.name}")
|
||||||
ConfirmDeleteModal(photo=selected_photo),
|
|
||||||
self.handle_delete_photo_result
|
|
||||||
)
|
|
||||||
|
|
||||||
def handle_delete_photo_result(self, result: bool) -> None:
|
# Schedule async deletion
|
||||||
"""Callback that processes the delete photo modal result."""
|
self.call_later(self._async_delete_photo, selected_photo)
|
||||||
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):
|
async def _async_delete_photo(self, photo: Photo):
|
||||||
"""Deletes a photo asynchronously"""
|
"""Deletes a photo asynchronously"""
|
||||||
|
|
@ -489,22 +456,18 @@ class EditEntryScreen(Screen):
|
||||||
def action_edit_photo(self):
|
def action_edit_photo(self):
|
||||||
"""Edit selected photo using modal"""
|
"""Edit selected photo using modal"""
|
||||||
if not self.sidebar_focused or not self.sidebar_visible:
|
if not self.sidebar_focused or not self.sidebar_visible:
|
||||||
self.notify("Use F8 to open the sidebar first.", severity="warning")
|
self.notify("Use F9 to focus the sidebar before using this shortcut.", severity="warning")
|
||||||
return
|
return
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
# 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)
|
photos = self._load_photos_for_diary(self.diary_id)
|
||||||
if photo_index < 0 or photo_index >= len(photos):
|
if self.photo_list.highlighted >= len(photos):
|
||||||
self.notify("No photo selected", severity="warning")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
selected_photo = photos[photo_index]
|
selected_photo = photos[self.photo_list.highlighted]
|
||||||
|
|
||||||
# Open edit photo modal
|
# Open edit photo modal
|
||||||
self.app.push_screen(
|
self.app.push_screen(
|
||||||
|
|
@ -518,15 +481,13 @@ class EditEntryScreen(Screen):
|
||||||
self.notify("Edit photo cancelled")
|
self.notify("Edit photo cancelled")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get the selected photo with adjusted index
|
# Get the selected photo
|
||||||
photos = self._load_photos_for_diary(self.diary_id)
|
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 self.photo_list.highlighted >= len(photos):
|
||||||
|
|
||||||
if self.photo_list.highlighted is None or photo_index < 0 or photo_index >= len(photos):
|
|
||||||
self.notify("Photo no longer available", severity="error")
|
self.notify("Photo no longer available", severity="error")
|
||||||
return
|
return
|
||||||
|
|
||||||
selected_photo = photos[photo_index]
|
selected_photo = photos[self.photo_list.highlighted]
|
||||||
|
|
||||||
# Schedule async update
|
# Schedule async update
|
||||||
self.call_later(self._async_update_photo, selected_photo, result)
|
self.call_later(self._async_update_photo, selected_photo, result)
|
||||||
|
|
@ -543,7 +504,7 @@ class EditEntryScreen(Screen):
|
||||||
name=photo_data["name"],
|
name=photo_data["name"],
|
||||||
addition_date=original_photo.addition_date,
|
addition_date=original_photo.addition_date,
|
||||||
caption=photo_data["caption"],
|
caption=photo_data["caption"],
|
||||||
entries=original_photo.entries if original_photo.entries is not None else [],
|
entries=original_photo.entries,
|
||||||
id=original_photo.id
|
id=original_photo.id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
|
||||||
from textual.app import ComposeResult
|
from textual.app import ComposeResult
|
||||||
from textual.screen import Screen
|
from textual.screen import Screen
|
||||||
from textual.widgets import Static, Input, Button
|
from textual.widgets import Static, Input, Button
|
||||||
|
|
@ -48,58 +47,15 @@ class AddPhotoModal(Screen):
|
||||||
if not filepath.strip() or not name.strip():
|
if not filepath.strip() or not name.strip():
|
||||||
self.notify("File path and name are required", severity="error")
|
self.notify("File path and name are required", severity="error")
|
||||||
return
|
return
|
||||||
|
self.result = {
|
||||||
# Try to create the photo in the database
|
|
||||||
self.call_later(self._async_create_photo, {
|
|
||||||
"filepath": filepath.strip(),
|
"filepath": filepath.strip(),
|
||||||
"name": name.strip(),
|
"name": name.strip(),
|
||||||
"caption": caption.strip() if caption.strip() else None
|
"caption": caption.strip() if caption.strip() else None
|
||||||
})
|
}
|
||||||
|
self.dismiss()
|
||||||
elif event.button.id == "cancel-button":
|
elif event.button.id == "cancel-button":
|
||||||
self.dismiss()
|
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:
|
def handle_file_picker_result(self, result: str | None) -> None:
|
||||||
if result:
|
if result:
|
||||||
# Set the filepath input value
|
self.query_one("#filepath-input", Input).value = result
|
||||||
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")
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
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)
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
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()
|
|
||||||
|
|
@ -25,6 +25,7 @@ class FilePickerModal(Screen):
|
||||||
self.start_path = Path(start_path or os.getcwd())
|
self.start_path = Path(start_path or os.getcwd())
|
||||||
# Start one level up to make navigation easier
|
# Start one level up to make navigation easier
|
||||||
self.current_path = self.start_path.parent
|
self.current_path = self.start_path.parent
|
||||||
|
self.result = None
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
yield Container(
|
yield Container(
|
||||||
|
|
@ -44,8 +45,8 @@ class FilePickerModal(Screen):
|
||||||
# Check if it's an image file
|
# Check if it's an image file
|
||||||
image_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'}
|
image_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'}
|
||||||
if file_path.suffix.lower() in image_extensions:
|
if file_path.suffix.lower() in image_extensions:
|
||||||
# Return the file path as result
|
self.result = str(file_path)
|
||||||
self.dismiss(str(file_path))
|
self.dismiss()
|
||||||
else:
|
else:
|
||||||
self.notify("Please select an image file", severity="warning")
|
self.notify("Please select an image file", severity="warning")
|
||||||
|
|
||||||
|
|
@ -62,5 +63,4 @@ class FilePickerModal(Screen):
|
||||||
tree.path = str(self.current_path)
|
tree.path = str(self.current_path)
|
||||||
tree.reload()
|
tree.reload()
|
||||||
elif event.button.id == "cancel-button":
|
elif event.button.id == "cancel-button":
|
||||||
# Return None to indicate cancellation
|
self.dismiss()
|
||||||
self.dismiss(None)
|
|
||||||
|
|
@ -589,41 +589,3 @@ Screen.-modal {
|
||||||
border: solid $accent;
|
border: solid $accent;
|
||||||
margin: 1;
|
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;
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue