Compare commits

...
20 Commits
Author SHA1 Message Date
Gustavo Henrique Santos Souza de Miranda f2b5f8b743 Merge branch 'fix/Importing-duplicate-images' into development
Merging the fix for the duplicate photo hash bugha
2025-07-19 01:04:03 -03:00
Gustavo Henrique Santos Souza de Miranda 9e4b08234f Fix the bug that collides the hash if identical images 2025-07-19 00:16:29 -03:00
Gustavo Henrique Miranda 44ff390695 Merge pull request #59 from gmbrax/fix/remove-stale-hashing-methods
Refactor the edit and add photo modal and remove the hashing methods out of the modals
2025-07-18 02:41:29 -03:00
Gustavo Henrique Santos Souza de Miranda ca53ac7778 Remove the unused import and unused variable 2025-07-18 02:37:07 -03:00
Gustavo Henrique Santos Souza de Miranda 8f98cb6c47 Merge branch 'fix/remove-stale-hashing-methods' into development
Removed some stale methods used for hashing the photos and refactored the code where it used the method to used the service hashing instead
2025-07-18 02:29:18 -03:00
Gustavo Henrique Santos Souza de Miranda 0567495ff9 Refactor the edit and add photo modal and remove the hashing methods out of the modals 2025-07-18 02:27:16 -03:00
Gustavo Henrique Miranda 18ff157eb7 Merge pull request #58 from gmbrax/fix/edit-photo-not-working
Fix the photo edit to properly pass the original photo hash that was missing
2025-07-18 01:39:13 -03:00
Gustavo Henrique Santos Souza de Miranda b8a5f2db15 Merge branch 'fix/edit-photo-not-working' into development
Merged the fix to allow the edit of the photos that was brokenakejillow d
2025-07-18 01:20:31 -03:00
Gustavo Henrique Santos Souza de Miranda a9d39f415a Fix the photo edit to properly pass the original photo hash that was missing 2025-07-18 00:03:13 -03:00
Gustavo Henrique Santos Souza de Miranda db3f7f846b Merge branch 'fix/sidebar-layout' into development
Merged the new sidebar layout and css to the development branch
2025-07-17 22:06:38 -03:00
Gustavo Henrique Miranda b14d775d87 Merge pull request #56 from gmbrax/fix/sidebar-layout
Improve the CSS and the code for the photo sidebar
2025-07-17 21:43:31 -03:00
Gustavo Henrique Santos Souza de Miranda 1d08f4e40d Change the CSS to better distribute the elements of the sidebar 2025-07-17 21:39:05 -03:00
Gustavo Henrique Santos Souza de Miranda 165bc10a63 Change the CSS to fit the text better and also limited the width of the sidebar 2025-07-17 20:48:59 -03:00
Gustavo Henrique Santos Souza de Miranda 71d5491233 Improve the CSS and the code for the photo sidebar 2025-07-17 20:32:54 -03:00
Gustavo Henrique Miranda a59af26c9d Merge pull request #55 from gmbrax/feat/use-a-toml-for-config
Feat/use a toml for config
2025-07-16 22:50:30 -03:00
Gustavo Henrique Miranda 26ef00fe3e Merge pull request #54 from gmbrax/feat/use-a-toml-for-config
Feat/use a toml for config
2025-07-16 22:16:32 -03:00
Gustavo Henrique Santos Souza de Miranda 23ef60b55a Improve exception handling, remove debug print statements 2025-07-16 18:28:42 -03:00
Gustavo Henrique Miranda 10135b9cdc Merge pull request #53 from gmbrax/staging
Staging
2025-07-16 01:34:56 -03:00
Gustavo Henrique Miranda 69c2fcfde6 Merge pull request #51 from gmbrax/fix/move-create-diary-to-modal
Change to call the method in the super class on the _on_screen_resume method
2025-07-16 00:54:48 -03:00
Gustavo Henrique Miranda 35f37f5926 Merge pull request #46 from gmbrax/fix/move-create-diary-to-modal
Fix/move create diary to modal
2025-07-15 17:28:22 -03:00
7 changed files with 75 additions and 74 deletions
+4
View File
@@ -4,6 +4,7 @@ from pathlib import Path
from sqlalchemy import Column, Integer, String, ForeignKey, DateTime from sqlalchemy import Column, Integer, String, ForeignKey, DateTime
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy.sql.schema import Index
from pilgrim.models.photo_in_entry import photo_entry_association from pilgrim.models.photo_in_entry import photo_entry_association
from ..database import Base from ..database import Base
@@ -24,6 +25,9 @@ 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)
__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): def __init__(self, filepath, name, photo_hash, addition_date=None, caption=None, entries=None, fk_travel_diary_id=None, **kw: Any):
super().__init__(**kw) super().__init__(**kw)
+12 -5
View File
@@ -14,8 +14,9 @@ class PhotoService:
def __init__(self, session): def __init__(self, session):
self.session = session self.session = session
def _hash_file(self, filepath: Path) -> str: @staticmethod
"""Calculate hash of a file using SHA3-384.""" def hash_file(filepath: Path) -> str:
"""Calculate the hash of a file using SHA3-384."""
hash_func = hashlib.new('sha3_384') hash_func = hashlib.new('sha3_384')
with open(filepath, 'rb') as f: with open(filepath, 'rb') as f:
while chunk := f.read(8192): while chunk := f.read(8192):
@@ -64,10 +65,18 @@ class PhotoService:
return dest_path 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: 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() 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
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 # Copy photo to diary's images directory
copied_path = self._copy_photo_to_diary(filepath, travel_diary) copied_path = self._copy_photo_to_diary(filepath, travel_diary)
@@ -79,8 +88,6 @@ class PhotoService:
except ValueError: except ValueError:
addition_date = None addition_date = None
# Calculate hash from the copied file
photo_hash = self._hash_file(copied_path)
new_photo = Photo( new_photo = Photo(
filepath=str(copied_path), # Store the path to the copied file filepath=str(copied_path), # Store the path to the copied file
@@ -118,7 +125,7 @@ class PhotoService:
old_path.unlink() old_path.unlink()
original.filepath = str(new_path) original.filepath = str(new_path)
# Update hash based on the new copied file # 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.name = photo_dst.name
original.addition_date = photo_dst.addition_date original.addition_date = photo_dst.addition_date
+15 -18
View File
@@ -87,7 +87,7 @@ class EditEntryScreen(Screen):
self.text_entry = TextArea(id="text_entry", classes="EditEntryScreen-text-entry") self.text_entry = TextArea(id="text_entry", classes="EditEntryScreen-text-entry")
# Sidebar widgets # 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_list = OptionList(id="photo_list", classes="EditEntryScreen-sidebar-photo-list")
self.photo_info = Static("", classes="EditEntryScreen-sidebar-photo-info") self.photo_info = Static("", classes="EditEntryScreen-sidebar-photo-info")
self.help_text = Static("", classes="EditEntryScreen-sidebar-help") self.help_text = Static("", classes="EditEntryScreen-sidebar-help")
@@ -296,30 +296,28 @@ class EditEntryScreen(Screen):
if not self.cached_photos: if not self.cached_photos:
self.photo_info.update("No photos found for this diary") 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 return
# Add photos to the list with hash # Add photos to the list with hash
for photo in self.cached_photos: for photo in self.cached_photos:
# Show name and hash in the list # Show name and hash in the list
photo_hash = str(photo.photo_hash)[:8] 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 # Updated help a text with hash information
help_text = ( help_text = (
"[b]⌨️ Sidebar Shortcuts[/b]\n" "[b]Sidebar Shortcuts[/b]\n"
"[b][green]i[/green][/b]: Insert photo into entry\n" "[b][green]i[/green][/b]: Insert photo into entry\n"
"[b][green]n[/green][/b]: Add new photo\n" "[b][green]n[/green][/b]: Add new photo\n"
"[b][green]d[/green][/b]: Delete selected photo\n" "[b][green]d[/green][/b]: Delete selected photo\n"
"[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)\n\n"
"[b]📝 Photo References[/b]\n" "[b]📝 Photo References[/b]\n"
"Use: \\[\\[photo:name:hash\\]\\]\n" "\\[\\[photo::hash\\]\\]"
"Or: \\[\\[photo::hash\\]\\]"
) )
self.help_text.update(help_text) self.help_text.update(help_text)
except Exception as e: except Exception as e:
@@ -429,9 +427,7 @@ class EditEntryScreen(Screen):
photo_details += f"🔗 {photo_hash}\n" photo_details += f"🔗 {photo_hash}\n"
photo_details += f"📅 {selected_photo.addition_date}\n" photo_details += f"📅 {selected_photo.addition_date}\n"
photo_details += f"💬 {selected_photo.caption or 'No caption'}\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"[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) self.photo_info.update(photo_details)
@@ -615,7 +611,8 @@ class EditEntryScreen(Screen):
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 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) 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}\\]") self.notify(f"Selected photo: {selected_photo.name} \\[{photo_hash}\\]")
# Update photo info with details including hash # Update photo info with details including hash
photo_details = f"📷 {selected_photo.name}\n" photo_details = f"Name: {selected_photo.name}\n"
photo_details += f"🔗 {photo_hash}\n" photo_details += f"Hash: {photo_hash}\n"
photo_details += f"📅 {selected_photo.addition_date}\n" photo_details += f"Date: {selected_photo.addition_date}\n"
if selected_photo.caption: if selected_photo.caption:
photo_details += f"💬 {selected_photo.caption}\n" photo_details += f"Caption: {selected_photo.caption}\n"
photo_details += f"📁 {selected_photo.filepath}\n\n" else:
photo_details += f"Caption: No Caption\n"
photo_details += f"[b]Reference formats:[/b]\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) self.photo_info.update(photo_details)
@@ -15,12 +15,6 @@ class AddPhotoModal(Screen):
self.result = None self.result = None
self.created_photo = 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: def compose(self) -> ComposeResult:
yield Container( yield Container(
@@ -69,10 +63,16 @@ class AddPhotoModal(Screen):
async def _async_create_photo(self, photo_data: dict): async def _async_create_photo(self, photo_data: dict):
"""Creates a new photo asynchronously using PhotoService""" """Creates a new photo asynchronously using PhotoService"""
try: try:
service_manager = self.app.service_manager service_manager = self.app.service_manager
photo_service = service_manager.get_photo_service() 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( new_photo = photo_service.create(
filepath=Path(photo_data["filepath"]), filepath=Path(photo_data["filepath"]),
name=photo_data["name"], name=photo_data["name"],
@@ -82,13 +82,9 @@ class AddPhotoModal(Screen):
if new_photo: if new_photo:
self.created_photo = 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) severity="information", timeout=5)
# Return the created photo data to the calling screen # Return the created photo data to the calling screen
@@ -97,7 +93,7 @@ class AddPhotoModal(Screen):
"name": photo_data["name"], "name": photo_data["name"],
"caption": photo_data["caption"], "caption": photo_data["caption"],
"photo_id": new_photo.id, "photo_id": new_photo.id,
"hash": photo_hash "hash": new_photo.photo_hash
} }
self.dismiss(self.result) self.dismiss(self.result)
else: else:
@@ -3,7 +3,7 @@ from textual.screen import Screen
from textual.widgets import Static, Input, Button from textual.widgets import Static, Input, Button
from textual.containers import Container, Horizontal from textual.containers import Container, Horizontal
from pilgrim.models.photo import Photo from pilgrim.models.photo import Photo
import hashlib
class EditPhotoModal(Screen): class EditPhotoModal(Screen):
"""Modal for editing an existing photo (name and caption only)""" """Modal for editing an existing photo (name and caption only)"""
@@ -12,15 +12,11 @@ class EditPhotoModal(Screen):
self.photo = photo self.photo = photo
self.result = None 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: def compose(self) -> ComposeResult:
# Generate hash for this photo # Generate hash for this photo
photo_hash = self._generate_photo_hash(self.photo)
yield Container( yield Container(
Static("✏️ Edit Photo", classes="EditPhotoModal-Title"), Static("✏️ Edit Photo", classes="EditPhotoModal-Title"),
@@ -45,10 +41,9 @@ class EditPhotoModal(Screen):
id="caption-input", id="caption-input",
classes="EditPhotoModal-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("Reference formats:", classes="EditPhotoModal-Label"),
Static(f"\\[\\[photo:{self.photo.name}:{photo_hash}\\]\\]", classes="EditPhotoModal-Reference"), Static(f"\\[\\[photo::{self.photo.photo_hash[:8]}\\]\\]", classes="EditPhotoModal-Reference"),
Static(f"\\[\\[photo::{photo_hash}\\]\\]", classes="EditPhotoModal-Reference"),
Horizontal( Horizontal(
Button("Save Changes", id="save-button", classes="EditPhotoModal-Button"), Button("Save Changes", id="save-button", classes="EditPhotoModal-Button"),
Button("Cancel", id="cancel-button", classes="EditPhotoModal-Button"), Button("Cancel", id="cancel-button", classes="EditPhotoModal-Button"),
+17 -20
View File
@@ -390,21 +390,9 @@ Screen.-modal {
} }
.EditEntryScreen-sidebar{ .EditEntryScreen-sidebar{
width: 40; background: $primary-darken-1;
min-height: 10; width: 45%;
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 { .EditEntryScreen-sidebar-title {
text-align: center; text-align: center;
text-style: bold; text-style: bold;
@@ -417,30 +405,39 @@ Screen.-modal {
.EditEntryScreen-sidebar-content { .EditEntryScreen-sidebar-content {
height: 1fr; height: 1fr;
layout: vertical; layout: vertical;
margin-left: 1;
margin-right: 1;
} }
.EditEntryScreen-sidebar-photo-list { .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; border: solid $accent;
margin-bottom: 1; margin-bottom: 1;
overflow-y: auto; /* Adiciona scroll vertical se necessário */
} }
.EditEntryScreen-sidebar-photo-info { .EditEntryScreen-sidebar-photo-info {
height: auto; height: 1fr;
min-height: 3; max-height: 15; /* Limita altura máxima */
border: solid $warning; min-height: 13;
padding: 1; padding: 1;
border: solid $warning;
margin-bottom: 1; margin-bottom: 1;
background: $surface-darken-1; background: $surface-darken-1;
overflow-y: auto; /* Adiciona scroll se exceder max-height */
} }
.EditEntryScreen-sidebar-help { .EditEntryScreen-sidebar-help {
height: auto; height: 1fr;
min-height: 8; max-height: 10; /* Altura máxima menor que info */
min-height: 8; /* Altura mínima menor */
border: solid $success; border: solid $success;
padding: 1; padding: 1;
background: $surface-darken-1; background: $surface-darken-1;
text-style: italic; text-style: italic;
overflow-y: auto; /* Adiciona scroll se exceder max-height */
} }
/* Photo Modal Styles */ /* Photo Modal Styles */
+11 -6
View File
@@ -34,11 +34,11 @@ class ConfigManager(metaclass=SingletonMeta):
try: try:
with open(f"{DirectoryManager.get_config_directory()}/config.toml", "rb") as f: with open(f"{DirectoryManager.get_config_directory()}/config.toml", "rb") as f:
data = tomli.load(f) data = tomli.load(f)
print(data)
except FileNotFoundError:
print("Error: config.toml not found.")
except tomli.TOMLDecodeError as e: 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.__data = data
self.database_url = self.__data["database"]["url"] self.database_url = self.__data["database"]["url"]
@@ -77,17 +77,22 @@ class ConfigManager(metaclass=SingletonMeta):
with open(f"{config_dir}/config.toml", "wb") as f: with open(f"{config_dir}/config.toml", "wb") as f:
tomli_w.dump(config, f) tomli_w.dump(config, f)
except Exception as e: except Exception as e:
print(f"Erro ao criar config: {e}") raise RuntimeError(f"Error creating configuration: {e}")
def save_config(self): def save_config(self):
if self.__data is None: if self.__data is None:
self.read_config() self.read_config()
if self.__data is None:
raise RuntimeError("Error reading configuration.")
self.__data["database"]["url"] = self.database_url self.__data["database"]["url"] = self.database_url
self.__data["database"]["type"] = self.database_type 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_diary_on_startup"] = self.auto_open_diary or ""
self.__data["settings"]["diary"]["auto_open_on_creation"] = self.auto_open_new_diary self.__data["settings"]["diary"]["auto_open_on_creation"] = self.auto_open_new_diary
try:
self.create_config(self.__data) self.create_config(self.__data)
except Exception as e:
raise RuntimeError(f"Error saving configuration: {e}")
def set_config_dir(self, value): def set_config_dir(self, value):
self.config_dir = value self.config_dir = value