Compare commits

..
57 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 1beeb39b95 Merge pull request #47 from gmbrax/fix/move-create-diary-to-modal
Fix/move create diary to modal
2025-07-15 18:14:09 -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
Gustavo Henrique Miranda 4f153c5a5b Merge branch 'development' into fix/move-create-diary-to-modal 2025-07-15 17:25:06 -03:00
Gustavo Henrique Santos Souza de Miranda f838a5b421 Add the auto-open option to the new_diary_modal.py 2025-07-15 15:51:30 -03:00
Gustavo Henrique Santos Souza de Miranda 18760e174c Add a functionality that automatically opens the diary after creation, Change the responsibility of diary creation to the new_diary_modal.py instead of the diary_list_screen.py 2025-07-15 02:14:53 -03:00
Gustavo Henrique Miranda 3bef353939 Merge pull request #42
Fixed the edit diary modal to accept the key "enter" and automatically the function
2025-07-14 18:24:14 -03:00
Gustavo Henrique Miranda a80ecc24f5 Merge pull request #43
Fixed the new diary modal to accept the key "enter" and automatically the function
2025-07-14 18:23:58 -03:00
Gustavo Henrique Miranda 7f193d0fbb Merge pull request #44 from gmbrax/dependabot/pip/textual-approx-eq-4.0.0
origin/dependabot/pip/textual-approx-eq-4.0.0
2025-07-14 18:17:49 -03:00
Gustavo Henrique Miranda 7dfeec20f1 Merge pull request #45 from gmbrax/dependabot/pip/typing-extensions-4.14.1
origin/dependabot/pip/typing-extensions-4.14.1
2025-07-14 18:17:32 -03:00
Gustavo Henrique Miranda d1545ad2e2 Merge branch 'staging' into dependabot/pip/typing-extensions-4.14.1 2025-07-14 18:17:15 -03:00
Gustavo Henrique Miranda 7dbb89b692 Merge pull request #40 from gmbrax/fix/add-enter-binding-edit-diary
Fix/add enter binding edit diary
2025-07-14 16:11:29 -03:00
Gustavo Henrique Miranda 33987507ac Merge pull request #41 from gmbrax/fix/add-enter-binding-on-create-diary
Fix/add enter binding on create diary
2025-07-14 16:11:17 -03:00
Gustavo Henrique Miranda ff97929dc0 Merge pull request #39 from gmbrax/dependabot/pip/textual-approx-eq-4.0.0
Dependabot/pip/textual approx eq 4.0.0
2025-07-14 16:02:45 -03:00
Gustavo Henrique Miranda 5d26c5394d Merge branch 'development' into dependabot/pip/textual-approx-eq-4.0.0 2025-07-14 16:02:35 -03:00
dependabot[bot] a867744d45 Update textual requirement from ~=3.3.0 to ~=4.0.0
---
updated-dependencies:
- dependency-name: textual
  dependency-version: 4.0.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-14 18:29:28 +00:00
dependabot[bot] d2e69ae770 Bump typing-extensions from 4.14.0 to 4.14.1
Bumps [typing-extensions](https://github.com/python/typing_extensions) from 4.14.0 to 4.14.1.
- [Release notes](https://github.com/python/typing_extensions/releases)
- [Changelog](https://github.com/python/typing_extensions/blob/main/CHANGELOG.md)
- [Commits](https://github.com/python/typing_extensions/compare/4.14.0...4.14.1)

---
updated-dependencies:
- dependency-name: typing-extensions
  dependency-version: 4.14.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-14 17:45:35 +00:00
Gustavo Henrique Santos Souza de Miranda f174fd1d8a Fixed the edit diary modal to accept the key "enter" and automatically the function 2025-07-14 02:23:29 -03:00
Gustavo Henrique Santos Souza de Miranda c98a106d97 Fixed the new diary modal to accept the key "enter" and automatically the function 2025-07-14 02:06:25 -03:00
Gustavo Henrique Miranda 417bc0d3e5 Merge pull request #36
Fixed the bindings to properly work and changed some bindings to avoid conflicts with the system
2025-07-14 00:46:23 -03:00
Gustavo Henrique Miranda 1312674186 Merge pull request #35 from gmbrax/fix/bindings-not-properly-working
Fixed the bindings to properly work and changed some bindings to avoid conflicts with the system
2025-07-13 23:06:14 -03:00
Gustavo Henrique Santos Souza de Miranda afc6908f37 Fixed the bindings to properly work and changed some bindings to avoid conflicts with the system 2025-07-13 22:53:39 -03:00
Gustavo Henrique Miranda 2983be09b6 Merge pull request #33 from gmbrax/fix/force-rename-entry
Fixed the new entry creation flow by prompting for a title if none is  set.
2025-07-12 19:29:55 -03:00
Gustavo Henrique Miranda 8129ae77a6 Merge branch 'staging' into fix/force-rename-entry 2025-07-12 19:11:56 -03:00
Gustavo Henrique Miranda 8e04c5b3c7 Merge pull request #32 from gmbrax/fix/create-a-new-entry-on-open
Corrected the behavior to create a new entry upon opening a diary
2025-07-12 13:31:25 -03:00
Gustavo Henrique Miranda fcaf17b593 Update README.md 2025-07-08 19:01:26 -03:00
Gustavo Henrique Miranda 1e07ea49df Update README.md 2025-07-08 19:01:11 -03:00
19 changed files with 377 additions and 194 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ structured and accessible format.
To install the application you must issue the command:
```bash
pip install pilgrim
pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple Pilgrim
```
## Usage
+4 -1
View File
@@ -5,7 +5,7 @@
[project]
name = "Pilgrim"
version = "0.0.3"
version = "0.0.4"
authors = [
{ name="Gustavo Henrique Santos Souza de Miranda", email="gustavohssmiranda@gmail.com" }
]
@@ -20,6 +20,9 @@
dependencies = [
"sqlalchemy",
"textual",
"tomli",
"tomli_w"
]
[template.plugins.default]
+1 -1
View File
@@ -3,4 +3,4 @@ SQLAlchemy==2.0.41
typing_extensions==4.14.1
textual~=3.6.0
textual~=4.0.0
+6 -4
View File
@@ -1,19 +1,21 @@
from pilgrim.database import Database
from pilgrim.service.servicemanager import ServiceManager
from pilgrim.ui.ui import UIApp
from pilgrim.utils import DirectoryManager
from pilgrim.utils import ConfigManager
class Application:
def __init__(self):
self.config_dir = DirectoryManager.get_config_directory()
self.database = Database()
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.ui = UIApp(session_manager, self.config_manager)
def run(self):
print(f"URL do banco: {self.config_manager.database_url}")
self.database.create()
self.ui.run()
+12 -26
View File
@@ -5,38 +5,24 @@ from pathlib import Path
import os
import shutil
from pilgrim.utils import ConfigManager
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:
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(
f"sqlite:///{db_path}",
f"sqlite:///{self.db_path}",
echo=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.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)
+12 -5
View File
@@ -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
+2 -2
View File
@@ -3,7 +3,7 @@ from textual.binding import Binding
from textual.screen import Screen
from textual.widgets import Header, Footer, Button, Label, TextArea
from textual.containers import Container
from importlib.metadata import version
class AboutScreen(Screen[bool]):
"""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.content = Label("A TUI Based Travel Diary Application", id="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")
self.developer = Label("Developed By: Gustavo Henrique Miranda ", id="AboutScreen_AboutAuthor")
self.contact = Label("git.gustavomiranda.xyz", id="AboutScreen_AboutContact",
+9 -20
View File
@@ -207,28 +207,17 @@ class DiaryListScreen(Screen):
self.app.push_screen(NewDiaryModal(),self._on_new_diary_submitted)
def _on_new_diary_submitted(self, result):
self.notify(str(result))
if result:
self.notify(f"Creating Diary:{result}'...")
self.call_later(self._async_create_diary,result)
"""Callback after diary creation"""
if result: # Se result não é string vazia, o diário foi criado
self.notify(f"Returning to diary list...")
# Atualiza a lista de diários
self.refresh_diaries()
else:
self.notify(f"Canceled...")
async def _async_create_diary(self,name: str):
try:
service = self.app.service_manager.get_travel_diary_service()
created_diary = await service.async_create(name)
if created_diary:
self.diary_id_map[created_diary.id] = created_diary.id
await self.async_refresh_diaries()
self.notify(f"Diary: '{name}' created!")
else:
self.notify("Error Creating the diary")
except Exception as e:
self.notify(f"Exception on creating the diary: {str(e)}")
self.notify(f"Creation canceled...")
def _on_screen_resume(self) -> None:
super()._on_screen_resume()
self.refresh_diaries()
def action_edit_selected_diary(self):
"""Action to edit selected diary"""
+14 -2
View File
@@ -8,6 +8,7 @@ from textual.widgets import Label, Input, Button
class EditDiaryModal(ModalScreen[tuple[int,str]]):
BINDINGS = [
Binding("escape", "cancel", "Cancel"),
Binding("enter", "edit_diary", "Save",priority=True),
]
def __init__(self, diary_id: int):
@@ -32,6 +33,15 @@ class EditDiaryModal(ModalScreen[tuple[int,str]]):
def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "save_diary_button":
self.action_edit_diary()
elif event.button.id == "cancel_button":
self.dismiss(None)
def on_key(self, event):
if event.key == "enter":
self.action_edit_diary()
event.prevent_default()
def action_edit_diary(self) -> None:
new_diary_name = self.name_input.value.strip()
if new_diary_name and new_diary_name != self.current_diary_name:
self.dismiss((self.diary_id, new_diary_name))
@@ -41,8 +51,10 @@ class EditDiaryModal(ModalScreen[tuple[int,str]]):
else:
self.notify("Diary name cannot be empty.", severity="warning")
self.name_input.focus()
elif event.button.id == "cancel_button":
self.dismiss(None)
def on_input_submitted(self, event: Input.Submitted) -> None:
if event.input.id == "edit_diary_name_input":
self.action_edit_diary()
def action_cancel(self) -> None:
self.dismiss(None)
+68 -45
View File
@@ -24,26 +24,33 @@ class EditEntryScreen(Screen):
BINDINGS = [
Binding("ctrl+q", "quit", "Quit"),
Binding("ctrl+s", "save", "Save"),
Binding("ctrl+n", "new_entry", "New Entry"),
Binding("ctrl+shift+n", "next_entry", "Next Entry"),
Binding("ctrl+shift+p", "prev_entry", "Previous Entry"),
Binding("shift+f5", "new_entry", "New Entry"),
Binding("f5", "next_entry", "Next Entry"),
Binding("f4", "prev_entry", "Previous Entry"),
Binding("ctrl+r", "rename_entry", "Rename Entry"),
Binding("f8", "toggle_sidebar", "Toggle Photos"),
Binding("f9", "toggle_focus", "Toggle Focus"),
Binding("escape", "back_to_list", "Back to List"),
]
def __init__(self, diary_id: int = 1):
def __init__(self, diary_id: int = 1,create_new: bool = True):
super().__init__()
if create_new:
self.current_entry_index = -1
self.is_new_entry = True
self.next_entry_id = None
else:
self.is_new_entry = False
self.current_entry_index = 0
self.next_entry_id = 1
self.new_entry_title = ""
self.new_entry_content = ""
self.diary_id = diary_id
self.diary_name = f"Diary {diary_id}"
self.current_entry_index = 0
self.entries: List[Entry] = []
self.is_new_entry = False
self.has_unsaved_changes = False
self.new_entry_content = ""
self.new_entry_title = "New Entry"
self.next_entry_id = 1
self._updating_display = False
self._original_content = ""
self.is_refreshing = False
@@ -80,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")
@@ -289,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:
@@ -422,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)
@@ -608,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)
@@ -735,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)
@@ -906,11 +910,22 @@ class EditEntryScreen(Screen):
self.notify("Empty entry cannot be saved")
return
# Passe a lista de fotos para o método de criação
if self.new_entry_title == "":
self.app.push_screen(RenameEntryModal(current_name=""), lambda result: self._handle_save_after_rename(result,content,
photos_to_link))
else:
self.call_later(self._async_create_entry, content, photos_to_link)
else:
# Passe a lista de fotos para o método de atualização
self.call_later(self._async_update_entry, content, photos_to_link)
def _handle_save_after_rename(self, result: str | None, content: str, photos_to_link: List[Photo]) -> None:
if result is None:
self.notify("Save cancelled")
return
self.new_entry_title = result
self.call_later(self._async_create_entry, content, photos_to_link)
async def _async_create_entry(self, content: str, photos_to_link: List[Photo]):
"""Creates a new entry and links the referenced photos."""
try:
@@ -937,7 +952,7 @@ class EditEntryScreen(Screen):
self.is_new_entry = False
self.has_unsaved_changes = False
self._original_content = new_entry.text
self.new_entry_title = "New Entry"
self.new_entry_title = ""
self.next_entry_id = max(entry.id for entry in self.entries) + 1
self._update_entry_display()
@@ -982,28 +997,36 @@ class EditEntryScreen(Screen):
except Exception as e:
self.notify(f"Error updating entry: {str(e)}")
def on_key(self, event):
# Sidebar contextual shortcuts
def check_key(self, event):
"""Check for custom key handling before bindings are processed"""
# Sidebar shortcuts
if self.sidebar_focused and self.sidebar_visible:
sidebar_keys = ["i", "n", "d", "e"]
if event.key in sidebar_keys:
if event.key == "i":
self.action_insert_photo()
event.stop()
elif event.key == "n":
self.action_ingest_new_photo()
event.stop()
elif event.key == "d":
self.action_delete_photo()
event.stop()
elif event.key == "e":
self.action_edit_photo()
event.stop()
# Shift+Tab: remove indent
elif self.focused is self.text_entry and event.key == "shift+tab":
return True # Indica que o evento foi processado
# Text area shortcuts
elif self.focused is self.text_entry:
if event.key in ["tab", "shift+tab"]:
if event.key == "shift+tab":
self._handle_shift_tab()
elif event.key == "tab":
self.text_entry.insert('\t')
return True # Indica que o evento foi processado
return False # Não foi processado, continuar com bindings
def _handle_shift_tab(self):
"""Handle shift+tab for removing indentation"""
textarea = self.text_entry
row, col = textarea.cursor_location
lines = textarea.text.splitlines()
@@ -1023,11 +1046,11 @@ class EditEntryScreen(Screen):
lines[row] = line[to_remove:]
textarea.text = '\n'.join(lines)
textarea.cursor_location = (row, max(col - to_remove, 0))
def on_key(self, event):
if self.check_key(event):
event.stop()
# Tab: insert tab
elif self.focused is self.text_entry and event.key == "tab":
self.text_entry.insert('\t')
event.stop()
return
def on_footer_action(self, event) -> None:
"""Handle clicks on footer actions (Textual 3.x)."""
@@ -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"),
+42 -7
View File
@@ -4,13 +4,17 @@ from textual.containers import Vertical, Horizontal
from textual.screen import ModalScreen
from textual.widgets import Label, Input, Button
from pilgrim.ui.screens.edit_entry_screen import EditEntryScreen
class NewDiaryModal(ModalScreen[str]):
BINDINGS = [
Binding("escape", "cancel", "Cancel"),
Binding("enter", "create_diary", "Create",priority=True),
]
def __init__(self):
super().__init__()
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:
@@ -31,15 +35,46 @@ class NewDiaryModal(ModalScreen[str]):
def on_button_pressed(self, event: Button.Pressed) -> None:
"""Handles button clicks."""
if event.button.id == "create_diary_button":
diary_name = self.name_input.value.strip()
if diary_name:
self.dismiss(diary_name)
else:
self.notify("Diary name cannot be empty.", severity="warning")
self.name_input.focus()
self.action_create_diary()
elif event.button.id == "cancel_button":
self.dismiss("")
self.dismiss()
def action_cancel(self) -> None:
"""Action to cancel the modal."""
self.dismiss("")
def action_create_diary(self) -> None:
diary_name = self.name_input.value.strip()
if diary_name:
self.call_later(self._async_create_diary, diary_name)
else:
self.notify("Diary name cannot be empty.", severity="warning")
self.name_input.focus()
def on_input_submitted(self, event: Input.Submitted) -> None:
if event.input.id == "NewDiaryModal-NameInput":
self.action_create_diary()
async def _async_create_diary(self, name: str):
try:
service = self.app.service_manager.get_travel_diary_service()
created_diary = await service.async_create(name)
if created_diary:
self.dismiss(name)
if self.auto_open:
self.app.push_screen(EditEntryScreen(diary_id=created_diary.id))
self.notify(f"Diary: '{name}' created!")
else:
self.notify("Error Creating the diary")
except Exception as e:
self.notify(f"Exception on creating the diary: {str(e)}")
+17 -20
View File
@@ -390,21 +390,9 @@ Screen.-modal {
}
.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;
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 */
+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.diary_list_screen import DiaryListScreen
from pilgrim.ui.screens.edit_entry_screen import EditEntryScreen
from pilgrim.utils import ConfigManager
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):
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)
self.service_manager = service_manager
self.config_manager = config_manager
def on_mount(self) -> None:
+2 -1
View File
@@ -1,3 +1,4 @@
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 shutil
from pathlib import Path
@@ -37,3 +38,26 @@ class DirectoryManager:
def get_diary_images_directory(directory_name: str) -> Path:
"""Returns the images directory path for a specific diary."""
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