mirror of
https://github.com/gmbrax/Pilgrim.git
synced 2026-11-10 06:00:17 +00:00
Compare commits
23
Commits
@@ -7,14 +7,15 @@ from pilgrim.utils import ConfigManager
|
||||
class Application:
|
||||
def __init__(self):
|
||||
self.config_manager = ConfigManager()
|
||||
self.config_manager.read_config() # Chamar antes de criar o Database
|
||||
self.database = Database(self.config_manager)
|
||||
session = self.database.session()
|
||||
session_manager = ServiceManager()
|
||||
session_manager.set_session(session)
|
||||
self.ui = UIApp(session_manager,self.config_manager)
|
||||
self.ui = UIApp(session_manager, self.config_manager)
|
||||
|
||||
def run(self):
|
||||
self.config_manager.read_config()
|
||||
print(f"URL do banco: {self.config_manager.database_url}")
|
||||
self.database.create()
|
||||
self.ui.run()
|
||||
|
||||
|
||||
+10
-3
@@ -12,10 +12,17 @@ Base = declarative_base()
|
||||
|
||||
|
||||
class Database:
|
||||
def __init__(self,config_manager:ConfigManager):
|
||||
db_path = config_manager.database_url
|
||||
|
||||
def __init__(self, config_manager: ConfigManager):
|
||||
self.db_path = config_manager.database_url
|
||||
|
||||
# Garantir que o diretório existe
|
||||
db_dir = os.path.dirname(self.db_path)
|
||||
if not os.path.exists(db_dir):
|
||||
os.makedirs(db_dir, exist_ok=True)
|
||||
|
||||
self.engine = create_engine(
|
||||
f"sqlite:///{config_manager.database_url}",
|
||||
f"sqlite:///{self.db_path}",
|
||||
echo=False,
|
||||
connect_args={"check_same_thread": False},
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ from pathlib import Path
|
||||
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, DateTime
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql.schema import Index
|
||||
|
||||
from pilgrim.models.photo_in_entry import photo_entry_association
|
||||
from ..database import Base
|
||||
@@ -24,6 +25,9 @@ class Photo(Base):
|
||||
)
|
||||
|
||||
fk_travel_diary_id = Column(Integer, ForeignKey("travel_diaries.id"),nullable=False)
|
||||
__table_args__ = (
|
||||
Index('idx_photo_hash_diary', 'hash', 'fk_travel_diary_id'),
|
||||
)
|
||||
|
||||
def __init__(self, filepath, name, photo_hash, addition_date=None, caption=None, entries=None, fk_travel_diary_id=None, **kw: Any):
|
||||
super().__init__(**kw)
|
||||
|
||||
@@ -14,8 +14,9 @@ class PhotoService:
|
||||
def __init__(self, session):
|
||||
self.session = session
|
||||
|
||||
def _hash_file(self, filepath: Path) -> str:
|
||||
"""Calculate hash of a file using SHA3-384."""
|
||||
@staticmethod
|
||||
def hash_file(filepath: Path) -> str:
|
||||
"""Calculate the hash of a file using SHA3-384."""
|
||||
hash_func = hashlib.new('sha3_384')
|
||||
with open(filepath, 'rb') as f:
|
||||
while chunk := f.read(8192):
|
||||
@@ -64,10 +65,18 @@ class PhotoService:
|
||||
|
||||
return dest_path
|
||||
|
||||
def check_photo_by_hash(self, photohash:str, traveldiaryid:int):
|
||||
photo = (self.session.query(Photo).filter(Photo.photo_hash == photohash,Photo.fk_travel_diary_id == traveldiaryid)
|
||||
.first())
|
||||
return photo
|
||||
|
||||
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
|
||||
photo_hash = self.hash_file(filepath)
|
||||
if self.check_photo_by_hash(photo_hash, travel_diary_id):
|
||||
return None
|
||||
|
||||
# Copy photo to diary's images directory
|
||||
copied_path = self._copy_photo_to_diary(filepath, travel_diary)
|
||||
@@ -79,8 +88,6 @@ class PhotoService:
|
||||
except ValueError:
|
||||
addition_date = None
|
||||
|
||||
# Calculate hash from the copied file
|
||||
photo_hash = self._hash_file(copied_path)
|
||||
|
||||
new_photo = Photo(
|
||||
filepath=str(copied_path), # Store the path to the copied file
|
||||
@@ -118,7 +125,7 @@ class PhotoService:
|
||||
old_path.unlink()
|
||||
original.filepath = str(new_path)
|
||||
# Update hash based on the new copied file
|
||||
original.photo_hash = self._hash_file(new_path)
|
||||
original.photo_hash = self.hash_file(new_path)
|
||||
|
||||
original.name = photo_dst.name
|
||||
original.addition_date = photo_dst.addition_date
|
||||
|
||||
@@ -87,7 +87,7 @@ class EditEntryScreen(Screen):
|
||||
self.text_entry = TextArea(id="text_entry", classes="EditEntryScreen-text-entry")
|
||||
|
||||
# Sidebar widgets
|
||||
self.sidebar_title = Static("📸 Photos", classes="EditEntryScreen-sidebar-title")
|
||||
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")
|
||||
@@ -296,30 +296,28 @@ class EditEntryScreen(Screen):
|
||||
|
||||
if not self.cached_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")
|
||||
self.help_text.update("No photos available\n\nUse Photo Manager to add photos")
|
||||
return
|
||||
|
||||
# Add photos to the list with hash
|
||||
for photo in self.cached_photos:
|
||||
# Show name and hash in the list
|
||||
photo_hash = str(photo.photo_hash)[:8]
|
||||
self.photo_list.add_option(f"📷 {photo.name} \\[{photo_hash}\]")
|
||||
self.photo_list.add_option(f"{photo.name} \\[{photo_hash}\]")
|
||||
|
||||
self.photo_info.update(f"📸 {len(self.cached_photos)} photos in diary")
|
||||
self.photo_info.update(f"{len(self.cached_photos)} photos in diary")
|
||||
|
||||
# Updated help a text with hash information
|
||||
help_text = (
|
||||
"[b]⌨️ Sidebar Shortcuts[/b]\n"
|
||||
"[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)\n\n"
|
||||
"[b]📝 Photo References[/b]\n"
|
||||
"Use: \\[\\[photo:name:hash\\]\\]\n"
|
||||
"Or: \\[\\[photo::hash\\]\\]"
|
||||
"\\[\\[photo::hash\\]\\]"
|
||||
)
|
||||
self.help_text.update(help_text)
|
||||
except Exception as e:
|
||||
@@ -429,9 +427,7 @@ class EditEntryScreen(Screen):
|
||||
photo_details += f"🔗 {photo_hash}\n"
|
||||
photo_details += f"📅 {selected_photo.addition_date}\n"
|
||||
photo_details += f"💬 {selected_photo.caption or 'No caption'}\n"
|
||||
photo_details += f"📁 {selected_photo.filepath}\n\n"
|
||||
photo_details += f"[b]Reference formats:[/b]\n"
|
||||
photo_details += f"\\[\\[photo:{selected_photo.name}:{photo_hash}\\]\\]\n"
|
||||
photo_details += f"\\[\\[photo::{photo_hash}\\]\\]"
|
||||
|
||||
self.photo_info.update(photo_details)
|
||||
@@ -615,7 +611,8 @@ class EditEntryScreen(Screen):
|
||||
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
|
||||
id=original_photo.id,
|
||||
photo_hash=original_photo.photo_hash,
|
||||
)
|
||||
|
||||
result = photo_service.update(original_photo, updated_photo)
|
||||
@@ -742,15 +739,15 @@ class EditEntryScreen(Screen):
|
||||
self.notify(f"Selected photo: {selected_photo.name} \\[{photo_hash}\\]")
|
||||
|
||||
# Update photo info with details including hash
|
||||
photo_details = f"📷 {selected_photo.name}\n"
|
||||
photo_details += f"🔗 {photo_hash}\n"
|
||||
photo_details += f"📅 {selected_photo.addition_date}\n"
|
||||
photo_details = f"Name: {selected_photo.name}\n"
|
||||
photo_details += f"Hash: {photo_hash}\n"
|
||||
photo_details += f"Date: {selected_photo.addition_date}\n"
|
||||
if selected_photo.caption:
|
||||
photo_details += f"💬 {selected_photo.caption}\n"
|
||||
photo_details += f"📁 {selected_photo.filepath}\n\n"
|
||||
photo_details += f"Caption: {selected_photo.caption}\n"
|
||||
else:
|
||||
photo_details += f"Caption: No Caption\n"
|
||||
photo_details += f"[b]Reference formats:[/b]\n"
|
||||
photo_details += f"\\[\\[photo:{selected_photo.name}:{photo_hash}\\]\\]\n"
|
||||
photo_details += f"\\[\\[photo::{photo_hash}\\]\\]"
|
||||
photo_details += f"\\[\\[photo::{photo_hash}]]"
|
||||
|
||||
self.photo_info.update(photo_details)
|
||||
|
||||
|
||||
@@ -15,12 +15,6 @@ class AddPhotoModal(Screen):
|
||||
self.result = None
|
||||
self.created_photo = None
|
||||
|
||||
def _generate_photo_hash(self, photo_data: dict) -> str:
|
||||
"""Generate a short, unique hash for a photo"""
|
||||
# Use temporary data for hash generation
|
||||
unique_string = f"{photo_data['name']}_{photo_data.get('photo_id', 0)}_new"
|
||||
hash_object = hashlib.md5(unique_string.encode())
|
||||
return hash_object.hexdigest()[:8]
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Container(
|
||||
@@ -69,10 +63,16 @@ class AddPhotoModal(Screen):
|
||||
|
||||
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()
|
||||
|
||||
if photo_service.check_photo_by_hash(photo_service.hash_file(photo_data["filepath"]),self.diary_id):
|
||||
self.notify("Photo already exists in database", severity="error")
|
||||
return
|
||||
|
||||
new_photo = photo_service.create(
|
||||
filepath=Path(photo_data["filepath"]),
|
||||
name=photo_data["name"],
|
||||
@@ -82,13 +82,9 @@ class AddPhotoModal(Screen):
|
||||
|
||||
if new_photo:
|
||||
self.created_photo = new_photo
|
||||
# Generate hash for the new photo
|
||||
photo_hash = self._generate_photo_hash({
|
||||
"name": new_photo.name,
|
||||
"photo_id": new_photo.id
|
||||
})
|
||||
|
||||
self.notify(f"Photo '{new_photo.name}' added successfully!\nHash: {photo_hash}\nReference: \\[\\[photo:{new_photo.name}:{photo_hash}\\]\\]",
|
||||
|
||||
self.notify(f"Photo '{new_photo.name}' added successfully!\nHash: {new_photo.photo_hash[:8]}\nReference: \\[\\[photo:{new_photo.name}:{new_photo.photo_hash[:8]}\\]\\]",
|
||||
severity="information", timeout=5)
|
||||
|
||||
# Return the created photo data to the calling screen
|
||||
@@ -97,7 +93,7 @@ class AddPhotoModal(Screen):
|
||||
"name": photo_data["name"],
|
||||
"caption": photo_data["caption"],
|
||||
"photo_id": new_photo.id,
|
||||
"hash": photo_hash
|
||||
"hash": new_photo.photo_hash
|
||||
}
|
||||
self.dismiss(self.result)
|
||||
else:
|
||||
|
||||
@@ -3,7 +3,7 @@ from textual.screen import Screen
|
||||
from textual.widgets import Static, Input, Button
|
||||
from textual.containers import Container, Horizontal
|
||||
from pilgrim.models.photo import Photo
|
||||
import hashlib
|
||||
|
||||
|
||||
class EditPhotoModal(Screen):
|
||||
"""Modal for editing an existing photo (name and caption only)"""
|
||||
@@ -12,15 +12,11 @@ class EditPhotoModal(Screen):
|
||||
self.photo = photo
|
||||
self.result = None
|
||||
|
||||
def _generate_photo_hash(self, photo: Photo) -> str:
|
||||
"""Generate a short, unique hash for a photo"""
|
||||
unique_string = f"{photo.name}_{photo.id}_{photo.addition_date}"
|
||||
hash_object = hashlib.md5(unique_string.encode())
|
||||
return hash_object.hexdigest()[:8]
|
||||
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
# Generate hash for this photo
|
||||
photo_hash = self._generate_photo_hash(self.photo)
|
||||
|
||||
|
||||
yield Container(
|
||||
Static("✏️ Edit Photo", classes="EditPhotoModal-Title"),
|
||||
@@ -45,10 +41,9 @@ class EditPhotoModal(Screen):
|
||||
id="caption-input",
|
||||
classes="EditPhotoModal-Input"
|
||||
),
|
||||
Static(f"🔗 Photo Hash: {photo_hash}", classes="EditPhotoModal-Hash"),
|
||||
Static(f"🔗 Photo Hash: {self.photo.photo_hash[:8]}", classes="EditPhotoModal-Hash"),
|
||||
Static("Reference formats:", classes="EditPhotoModal-Label"),
|
||||
Static(f"\\[\\[photo:{self.photo.name}:{photo_hash}\\]\\]", classes="EditPhotoModal-Reference"),
|
||||
Static(f"\\[\\[photo::{photo_hash}\\]\\]", classes="EditPhotoModal-Reference"),
|
||||
Static(f"\\[\\[photo::{self.photo.photo_hash[:8]}\\]\\]", classes="EditPhotoModal-Reference"),
|
||||
Horizontal(
|
||||
Button("Save Changes", id="save-button", classes="EditPhotoModal-Button"),
|
||||
Button("Cancel", id="cancel-button", classes="EditPhotoModal-Button"),
|
||||
|
||||
@@ -12,9 +12,9 @@ class NewDiaryModal(ModalScreen[str]):
|
||||
Binding("escape", "cancel", "Cancel"),
|
||||
Binding("enter", "create_diary", "Create",priority=True),
|
||||
]
|
||||
def __init__(self,autoopen=True):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.auto_open = autoopen
|
||||
self.auto_open = self.app.config_manager.auto_open_new_diary
|
||||
self.name_input = Input(id="NewDiaryModal-NameInput",classes="NewDiaryModal-NameInput") # This ID is fine, it's specific to the input
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
|
||||
@@ -389,22 +389,10 @@ Screen.-modal {
|
||||
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-sidebar{
|
||||
background: $primary-darken-1;
|
||||
width: 45%;
|
||||
}
|
||||
|
||||
.EditEntryScreen-content-container {
|
||||
layout: horizontal;
|
||||
height: 1fr;
|
||||
}
|
||||
|
||||
.EditEntryScreen-sidebar-title {
|
||||
text-align: center;
|
||||
text-style: bold;
|
||||
@@ -417,30 +405,39 @@ Screen.-modal {
|
||||
.EditEntryScreen-sidebar-content {
|
||||
height: 1fr;
|
||||
layout: vertical;
|
||||
margin-left: 1;
|
||||
margin-right: 1;
|
||||
|
||||
}
|
||||
|
||||
.EditEntryScreen-sidebar-photo-list {
|
||||
height: 1fr;
|
||||
height: 2fr; /* Pega o espaço restante disponível */
|
||||
min-height: 5; /* Garante altura mínima para não sumir */
|
||||
border: solid $accent;
|
||||
margin-bottom: 1;
|
||||
overflow-y: auto; /* Adiciona scroll vertical se necessário */
|
||||
}
|
||||
|
||||
.EditEntryScreen-sidebar-photo-info {
|
||||
height: auto;
|
||||
min-height: 3;
|
||||
border: solid $warning;
|
||||
height: 1fr;
|
||||
max-height: 15; /* Limita altura máxima */
|
||||
min-height: 13;
|
||||
padding: 1;
|
||||
border: solid $warning;
|
||||
margin-bottom: 1;
|
||||
background: $surface-darken-1;
|
||||
overflow-y: auto; /* Adiciona scroll se exceder max-height */
|
||||
}
|
||||
|
||||
.EditEntryScreen-sidebar-help {
|
||||
height: auto;
|
||||
min-height: 8;
|
||||
height: 1fr;
|
||||
max-height: 10; /* Altura máxima menor que info */
|
||||
min-height: 8; /* Altura mínima menor */
|
||||
border: solid $success;
|
||||
padding: 1;
|
||||
background: $surface-darken-1;
|
||||
text-style: italic;
|
||||
overflow-y: auto; /* Adiciona scroll se exceder max-height */
|
||||
}
|
||||
|
||||
/* Photo Modal Styles */
|
||||
|
||||
@@ -34,11 +34,11 @@ class ConfigManager(metaclass=SingletonMeta):
|
||||
try:
|
||||
with open(f"{DirectoryManager.get_config_directory()}/config.toml", "rb") as f:
|
||||
data = tomli.load(f)
|
||||
print(data)
|
||||
except FileNotFoundError:
|
||||
print("Error: config.toml not found.")
|
||||
|
||||
except tomli.TOMLDecodeError as e:
|
||||
print(f"Error decoding TOML: {e}")
|
||||
raise ValueError(f"Invalid TOML configuration: {e}")
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Error reading configuration: {e}")
|
||||
|
||||
self.__data = data
|
||||
self.database_url = self.__data["database"]["url"]
|
||||
@@ -53,9 +53,14 @@ class ConfigManager(metaclass=SingletonMeta):
|
||||
self.read_config()
|
||||
|
||||
def create_config(self, config: dict = None):
|
||||
# Garantir que o diretório de configuração existe
|
||||
config_dir = DirectoryManager.get_config_directory()
|
||||
if not os.path.exists(config_dir):
|
||||
os.makedirs(config_dir, exist_ok=True)
|
||||
|
||||
default = {
|
||||
"database": {
|
||||
"url": f"{DirectoryManager.get_config_directory()}/database.db",
|
||||
"url": f"{config_dir}/database.db",
|
||||
"type": "sqlite"
|
||||
},
|
||||
"settings": {
|
||||
@@ -67,21 +72,27 @@ class ConfigManager(metaclass=SingletonMeta):
|
||||
}
|
||||
if config is None:
|
||||
config = default
|
||||
|
||||
try:
|
||||
with open(f"{DirectoryManager.get_config_directory()}/config.toml", "wb") as f:
|
||||
with open(f"{config_dir}/config.toml", "wb") as f:
|
||||
tomli_w.dump(config, f)
|
||||
except FileNotFoundError:
|
||||
print("Error: config.toml not found.")
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Error creating configuration: {e}")
|
||||
|
||||
def save_config(self):
|
||||
if self.__data is None:
|
||||
self.read_config()
|
||||
if self.__data is None:
|
||||
raise RuntimeError("Error reading configuration.")
|
||||
|
||||
self.__data["database"]["url"] = self.database_url
|
||||
self.__data["database"]["type"] = self.database_type
|
||||
self.__data["settings"]["diary"]["auto_open_diary_on_startup"] = self.auto_open_diary or ""
|
||||
self.__data["settings"]["diary"]["auto_open_on_creation"] = self.auto_open_new_diary
|
||||
|
||||
self.create_config(self.__data)
|
||||
try:
|
||||
self.create_config(self.__data)
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Error saving configuration: {e}")
|
||||
|
||||
def set_config_dir(self, value):
|
||||
self.config_dir = value
|
||||
|
||||
@@ -45,11 +45,7 @@ class DirectoryManager:
|
||||
Get the database file path following XDG Base Directory specification.
|
||||
Creates the directory if it doesn't exist.
|
||||
"""
|
||||
|
||||
pilgrim_dir = DirectoryManager.get_config_directory()
|
||||
pilgrim_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Database file path
|
||||
db_path = pilgrim_dir / "database.db"
|
||||
|
||||
# If database doesn't exist in new location but exists in current directory,
|
||||
@@ -57,7 +53,11 @@ class DirectoryManager:
|
||||
if not db_path.exists():
|
||||
current_db = Path("database.db")
|
||||
if current_db.exists():
|
||||
shutil.copy2(current_db, db_path)
|
||||
print(f"Database migrated from {current_db} to {db_path}")
|
||||
try:
|
||||
shutil.copy2(current_db, db_path)
|
||||
# Consider using logging instead of print
|
||||
print(f"Database migrated from {current_db} to {db_path}")
|
||||
except (OSError, shutil.Error) as e:
|
||||
raise RuntimeError(f"Failed to migrate database: {e}")
|
||||
|
||||
return db_path
|
||||
|
||||
Reference in New Issue
Block a user