Compare commits

...
32 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 Mirandaandcoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> 4436d5e081 Update src/pilgrim/utils/directory_manager.py
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-07-16 18:12:32 -03:00
Gustavo Henrique Santos Souza de Miranda a13c56a3a3 Fix the NewDiaryModal not accessing the toml file properly to set the auto_open flag 2025-07-16 17:24:45 -03:00
Gustavo Henrique Santos Souza de Miranda 967c7e4ce6 Fix the Database no being put on the right path according with the config.toml 2025-07-16 16:39:31 -03:00
Gustavo Henrique Santos Souza de Miranda bdaa37e355 Add the database file path now is read by the configuration file 2025-07-16 16:01:34 -03:00
Gustavo Henrique Santos Souza de Miranda a540347127 Add Tomli_w library to pyproject.toml 2025-07-16 15:31:19 -03:00
Gustavo Henrique Santos Souza de Miranda fe35cb93bd Move get_database_path from database.py to directory_manager.py 2025-07-16 15:30:47 -03:00
Gustavo Henrique Santos Souza de Miranda 3843be6e13 Add the tomli library and create the config_manager.py to manage the configuration file and also add the dependency injection on the ui.py via the Application class 2025-07-16 02:55:52 -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 0adb230dd7 Merge pull request #52 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 01:30:35 -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 Santos Souza de Miranda 167c2d8928 Change to call the method in the super class on the _on_screen_resume method 2025-07-16 00:50:07 -03:00
Gustavo Henrique Miranda 5b0f894c88 Merge pull request #50 from gmbrax/fix/bump-version
Bump the version to 0.0.4 on about_screen.py and pyproject.toml
2025-07-16 00:42:42 -03:00
Gustavo Henrique Santos Souza de Miranda 2b851fc7c7 Add the use of importlib.metadata.version to keep the version string in sync with the pyproject.toml 2025-07-16 00:39:28 -03:00
Gustavo Henrique Santos Souza de Miranda a3123fe322 Bump the version to 0.0.4 on about_screen.py and pyproject.toml 2025-07-16 00:25:37 -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
16 changed files with 227 additions and 105 deletions
+4 -1
View File
@@ -5,7 +5,7 @@
[project] [project]
name = "Pilgrim" name = "Pilgrim"
version = "0.0.3" version = "0.0.4"
authors = [ authors = [
{ name="Gustavo Henrique Santos Souza de Miranda", email="gustavohssmiranda@gmail.com" } { name="Gustavo Henrique Santos Souza de Miranda", email="gustavohssmiranda@gmail.com" }
] ]
@@ -20,6 +20,9 @@
dependencies = [ dependencies = [
"sqlalchemy", "sqlalchemy",
"textual", "textual",
"tomli",
"tomli_w"
] ]
[template.plugins.default] [template.plugins.default]
+6 -4
View File
@@ -1,19 +1,21 @@
from pilgrim.database import Database from pilgrim.database import Database
from pilgrim.service.servicemanager import ServiceManager from pilgrim.service.servicemanager import ServiceManager
from pilgrim.ui.ui import UIApp from pilgrim.ui.ui import UIApp
from pilgrim.utils import DirectoryManager from pilgrim.utils import ConfigManager
class Application: class Application:
def __init__(self): def __init__(self):
self.config_dir = DirectoryManager.get_config_directory() self.config_manager = ConfigManager()
self.database = Database() self.config_manager.read_config() # Chamar antes de criar o Database
self.database = Database(self.config_manager)
session = self.database.session() session = self.database.session()
session_manager = ServiceManager() session_manager = ServiceManager()
session_manager.set_session(session) session_manager.set_session(session)
self.ui = UIApp(session_manager) self.ui = UIApp(session_manager, self.config_manager)
def run(self): def run(self):
print(f"URL do banco: {self.config_manager.database_url}")
self.database.create() self.database.create()
self.ui.run() self.ui.run()
+12 -26
View File
@@ -5,38 +5,24 @@ from pathlib import Path
import os import os
import shutil import shutil
from pilgrim.utils import ConfigManager
Base = declarative_base() Base = declarative_base()
def get_database_path() -> Path:
"""
Get the database file path following XDG Base Directory specification.
Creates the directory if it doesn't exist.
"""
# Get home directory
home = Path.home()
# Create .pilgrim directory if it doesn't exist
pilgrim_dir = home / ".pilgrim"
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,
# migrate it
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}")
return db_path
class Database: class Database:
def __init__(self):
db_path = get_database_path() 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( self.engine = create_engine(
f"sqlite:///{db_path}", f"sqlite:///{self.db_path}",
echo=False, echo=False,
connect_args={"check_same_thread": False}, connect_args={"check_same_thread": False},
) )
+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
+2 -2
View File
@@ -3,7 +3,7 @@ from textual.binding import Binding
from textual.screen import Screen from textual.screen import Screen
from textual.widgets import Header, Footer, Button, Label, TextArea from textual.widgets import Header, Footer, Button, Label, TextArea
from textual.containers import Container from textual.containers import Container
from importlib.metadata import version
class AboutScreen(Screen[bool]): class AboutScreen(Screen[bool]):
"""Screen to display application information.""" """Screen to display application information."""
@@ -20,7 +20,7 @@ class AboutScreen(Screen[bool]):
self.app_title = Label("Pilgrim", id="AboutScreen_AboutTitle",classes="AboutScreen_AboutTitle") self.app_title = Label("Pilgrim", id="AboutScreen_AboutTitle",classes="AboutScreen_AboutTitle")
self.content = Label("A TUI Based Travel Diary Application", id="AboutScreen_AboutContent", self.content = Label("A TUI Based Travel Diary Application", id="AboutScreen_AboutContent",
classes="AboutScreen_AboutContent") classes="AboutScreen_AboutContent")
self.version = Label("Version: 0.0.1", id="AboutScreen_AboutVersion", self.version = Label(f"Version: {version('Pilgrim')}", id="AboutScreen_AboutVersion",
classes="AboutScreen_AboutVersion") classes="AboutScreen_AboutVersion")
self.developer = Label("Developed By: Gustavo Henrique Miranda ", id="AboutScreen_AboutAuthor") self.developer = Label("Developed By: Gustavo Henrique Miranda ", id="AboutScreen_AboutAuthor")
self.contact = Label("git.gustavomiranda.xyz", id="AboutScreen_AboutContact", self.contact = Label("git.gustavomiranda.xyz", id="AboutScreen_AboutContact",
@@ -216,6 +216,7 @@ class DiaryListScreen(Screen):
self.notify(f"Creation canceled...") self.notify(f"Creation canceled...")
def _on_screen_resume(self) -> None: def _on_screen_resume(self) -> None:
super()._on_screen_resume()
self.refresh_diaries() self.refresh_diaries()
def action_edit_selected_diary(self): def action_edit_selected_diary(self):
+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"),
+2 -2
View File
@@ -12,9 +12,9 @@ class NewDiaryModal(ModalScreen[str]):
Binding("escape", "cancel", "Cancel"), Binding("escape", "cancel", "Cancel"),
Binding("enter", "create_diary", "Create",priority=True), Binding("enter", "create_diary", "Create",priority=True),
] ]
def __init__(self,autoopen=True): def __init__(self):
super().__init__() 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 self.name_input = Input(id="NewDiaryModal-NameInput",classes="NewDiaryModal-NameInput") # This ID is fine, it's specific to the input
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
+18 -21
View File
@@ -389,22 +389,10 @@ Screen.-modal {
width: 1fr; width: 1fr;
} }
.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 */
+3 -1
View File
@@ -9,6 +9,7 @@ from pilgrim.service.servicemanager import ServiceManager
from pilgrim.ui.screens.about_screen import AboutScreen from pilgrim.ui.screens.about_screen import AboutScreen
from pilgrim.ui.screens.diary_list_screen import DiaryListScreen from pilgrim.ui.screens.diary_list_screen import DiaryListScreen
from pilgrim.ui.screens.edit_entry_screen import EditEntryScreen from pilgrim.ui.screens.edit_entry_screen import EditEntryScreen
from pilgrim.utils import ConfigManager
CSS_FILE_PATH = Path(__file__).parent / "styles" / "pilgrim.css" CSS_FILE_PATH = Path(__file__).parent / "styles" / "pilgrim.css"
@@ -16,9 +17,10 @@ CSS_FILE_PATH = Path(__file__).parent / "styles" / "pilgrim.css"
class UIApp(App): class UIApp(App):
CSS_PATH = CSS_FILE_PATH CSS_PATH = CSS_FILE_PATH
def __init__(self,service_manager: ServiceManager, **kwargs): def __init__(self,service_manager: ServiceManager, config_manager: ConfigManager, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self.service_manager = service_manager self.service_manager = service_manager
self.config_manager = config_manager
def on_mount(self) -> None: def on_mount(self) -> None:
+2 -1
View File
@@ -1,3 +1,4 @@
from .directory_manager import DirectoryManager from .directory_manager import DirectoryManager
from .config_manager import ConfigManager
__all__ = ['DirectoryManager'] __all__ = ['DirectoryManager', 'ConfigManager']
+107
View File
@@ -0,0 +1,107 @@
import os.path
from os import PathLike
from threading import Lock
import tomli
import tomli_w
from pilgrim.utils import DirectoryManager
class SingletonMeta(type):
_instances = {}
_lock: Lock = Lock()
def __call__(cls, *args, **kwargs):
with cls._lock:
if cls not in cls._instances:
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls]
class ConfigManager(metaclass=SingletonMeta):
def __init__(self):
self.database_url = None
self.database_type = None
self.auto_open_diary = None
self.auto_open_new_diary = None
self.config_dir = DirectoryManager.get_config_directory()
self.__data = None
def read_config(self):
if os.path.exists(f"{DirectoryManager.get_config_directory()}/config.toml"):
try:
with open(f"{DirectoryManager.get_config_directory()}/config.toml", "rb") as f:
data = tomli.load(f)
except tomli.TOMLDecodeError as 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"]
self.database_type = self.__data["database"]["type"]
if self.__data["settings"]["diary"]["auto_open_diary_on_startup"] == "":
self.auto_open_diary = None
self.auto_open_new_diary = self.__data["settings"]["diary"]["auto_open_on_creation"]
else:
print("Error: config.toml not found.")
self.create_config()
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"{config_dir}/database.db",
"type": "sqlite"
},
"settings": {
"diary": {
"auto_open_diary_on_startup": "",
"auto_open_on_creation": False
}
}
}
if config is None:
config = default
try:
with open(f"{config_dir}/config.toml", "wb") as f:
tomli_w.dump(config, f)
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
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
def set_database_url(self, value: str):
self.database_url = value
def set_auto_open_diary(self, value: str):
self.auto_open_diary = value
def set_auto_open_new_diary(self, value: bool):
self.auto_open_new_diary = value
+24
View File
@@ -1,4 +1,5 @@
import os import os
import shutil
from pathlib import Path from pathlib import Path
@@ -37,3 +38,26 @@ class DirectoryManager:
def get_diary_images_directory(directory_name: str) -> Path: def get_diary_images_directory(directory_name: str) -> Path:
"""Returns the images directory path for a specific diary.""" """Returns the images directory path for a specific diary."""
return DirectoryManager.get_diary_data_directory(directory_name) / "images" return DirectoryManager.get_diary_data_directory(directory_name) / "images"
@staticmethod
def get_database_path() -> Path:
"""
Get the database file path following XDG Base Directory specification.
Creates the directory if it doesn't exist.
"""
pilgrim_dir = DirectoryManager.get_config_directory()
db_path = pilgrim_dir / "database.db"
# If database doesn't exist in new location but exists in current directory,
# migrate it
if not db_path.exists():
current_db = Path("database.db")
if current_db.exists():
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