mirror of https://github.com/gmbrax/Pilgrim.git
commit
f825236c45
|
|
@ -1,24 +0,0 @@
|
||||||
name: Pylint
|
|
||||||
|
|
||||||
on: [push]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
python-version: ["3.10"]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
|
||||||
uses: actions/setup-python@v3
|
|
||||||
with:
|
|
||||||
python-version: ${{ matrix.python-version }}
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
|
||||||
pip install pylint
|
|
||||||
- name: Analysing the code with pylint
|
|
||||||
run: |
|
|
||||||
pylint --disable=C0114,C0115,C0116 --exit-zero $(git ls-files '*.py')
|
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
name: Pylint and SonarCloud
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, master, staging ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main, master, staging ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.10"]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # Necessário para SonarCloud
|
||||||
|
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||||
|
pip install pylint ruff coverage pytest
|
||||||
|
|
||||||
|
- name: Analysing the code with pylint (console output)
|
||||||
|
run: |
|
||||||
|
pylint --disable=C0114,C0115,C0116 --exit-zero $(git ls-files '*.py') || true
|
||||||
|
|
||||||
|
- name: Generate Pylint report for SonarCloud
|
||||||
|
run: |
|
||||||
|
pylint --disable=C0114,C0115,C0116 --output-format=json --exit-zero $(git ls-files '*.py') > pylint-report.json || true
|
||||||
|
|
||||||
|
- name: Run Ruff
|
||||||
|
run: |
|
||||||
|
ruff check --output-format=json . > ruff-report.json || true
|
||||||
|
|
||||||
|
- name: Run tests with coverage (if you have tests)
|
||||||
|
run: |
|
||||||
|
if [ -d "tests" ] || [ -f "test_*.py" ] || [ -f "*_test.py" ]; then
|
||||||
|
coverage run -m pytest || true
|
||||||
|
coverage xml || true
|
||||||
|
else
|
||||||
|
echo "No tests found, skipping coverage"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: SonarCloud Scan
|
||||||
|
uses: SonarSource/sonarqube-scan-action@v5.0.0
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||||
|
with:
|
||||||
|
args: >
|
||||||
|
-Dsonar.projectKey=gmbrax_Pilgrim
|
||||||
|
-Dsonar.organization=gmbrax
|
||||||
|
-Dsonar.python.pylint.reportPaths=pylint-report.json
|
||||||
|
-Dsonar.python.ruff.reportPaths=ruff-report.json
|
||||||
|
-Dsonar.python.coverage.reportPaths=coverage.xml
|
||||||
77
CHANGELOG.md
77
CHANGELOG.md
|
|
@ -1,51 +1,68 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
## Unreleased
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
||||||
|
|
||||||
## [Unreleased]
|
## Planned
|
||||||
|
* Installation Method 1 (repository compilation)
|
||||||
|
* Organization of trips by date, location, or theme
|
||||||
|
* Enhanced photo management features
|
||||||
|
* Search functionality
|
||||||
|
* Export features
|
||||||
|
* Testing implementation
|
||||||
|
|
||||||
### Planned
|
## [0.0.4] - 2025-07-19
|
||||||
|
|
||||||
- Installation Method 1 (repository compilation)
|
### Added
|
||||||
- Organization of trips by date, location, or theme
|
* Support for creating new diaries asynchronously, with an option to automatically open the newly created diary
|
||||||
- Enhanced photo management features
|
* Unified "Enter" key support for saving or creating diaries across relevant modals
|
||||||
- Search functionality
|
* Automatic diary list refresh when returning to the diary screen
|
||||||
- Export features
|
* Application configuration management with a new centralized config system
|
||||||
- Testing implementation
|
* Database location and initialization now configurable via the new config manager
|
||||||
|
* Automatic migration of database file to the configuration directory
|
||||||
|
* Display of database URL on application startup for transparency
|
||||||
|
* Duplicate photo detection before photo creation to prevent redundant entries
|
||||||
|
* Photo hash indexing to improve photo lookup performance
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* Enhanced feedback and validation when editing or creating diary names
|
||||||
|
* Streamlined and unified save logic for diary modals, reducing duplicated behavior
|
||||||
|
* About screen now displays the actual installed application version dynamically
|
||||||
|
* Sidebar and photo-related UI text updated to remove emoji icons for a cleaner appearance
|
||||||
|
* Sidebar layout and scrolling behavior improved for better usability
|
||||||
|
* Photo hash generation now relies on existing service-provided hashes instead of local computation
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
* Enhanced feedback and validation when editing or creating diary names
|
||||||
|
* Streamlined and unified save logic for diary modals, reducing duplicated behavior
|
||||||
|
* Sidebar layout and scrolling behavior for better usability
|
||||||
|
|
||||||
## [0.0.3] - 2025-07-07
|
## [0.0.3] - 2025-07-07
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Removed the dependency on textual-dev from pyproject.toml
|
* Removed the dependency on textual-dev from pyproject.toml
|
||||||
|
|
||||||
## [0.0.2] - 2025-07-07
|
## [0.0.2] - 2025-07-07
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Changed the license in pyproject.toml to BSD
|
* Changed the license in pyproject.toml to BSD
|
||||||
|
|
||||||
## [0.0.1] - 2025-07-06
|
## [0.0.1] - 2025-07-06
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
* Initial alpha release of Pilgrim travel diary application
|
||||||
- Initial alpha release of Pilgrim travel diary application
|
* Create and edit travel diaries
|
||||||
- Create and edit travel diaries
|
* Create and edit diary entries
|
||||||
- Create and edit diary entries
|
* Photo ingestion system
|
||||||
- Photo ingestion system
|
* Photo addition and reference via sidebar
|
||||||
- Photo addition and reference via sidebar
|
* Text User Interface (TUI) built with Textual framework
|
||||||
- Text User Interface (TUI) built with Textual framework
|
* Pre-compiled binary installation method (Method 2)
|
||||||
- Pre-compiled binary installation method (Method 2)
|
* Support for Linux operating systems
|
||||||
- Support for Linux operating systems
|
* Basic project documentation (README)
|
||||||
- Basic project documentation (README)
|
|
||||||
|
|
||||||
### Known Issues
|
### Known Issues
|
||||||
|
* Installation Method 1 not yet implemented
|
||||||
- Installation Method 1 not yet implemented
|
* No testing suite implemented yet
|
||||||
- No testing suite implemented yet
|
* Some features may be unstable in an alpha version
|
||||||
- Some features may be unstable in an alpha version
|
|
||||||
|
|
||||||
[Unreleased]: https://github.com/username/pilgrim/compare/v0.0.1...HEAD
|
|
||||||
|
|
||||||
[0.0.1]: https://github.com/username/pilgrim/releases/tag/v0.0.1
|
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -206,29 +206,18 @@ class DiaryListScreen(Screen):
|
||||||
"""Action to create new diary"""
|
"""Action to create new diary"""
|
||||||
self.app.push_screen(NewDiaryModal(),self._on_new_diary_submitted)
|
self.app.push_screen(NewDiaryModal(),self._on_new_diary_submitted)
|
||||||
|
|
||||||
def _on_new_diary_submitted(self,result):
|
def _on_new_diary_submitted(self, result):
|
||||||
self.notify(str(result))
|
"""Callback after diary creation"""
|
||||||
if result:
|
if result: # Se result não é string vazia, o diário foi criado
|
||||||
self.notify(f"Creating Diary:{result}'...")
|
self.notify(f"Returning to diary list...")
|
||||||
self.call_later(self._async_create_diary,result)
|
# Atualiza a lista de diários
|
||||||
|
self.refresh_diaries()
|
||||||
else:
|
else:
|
||||||
self.notify(f"Canceled...")
|
self.notify(f"Creation 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)}")
|
|
||||||
|
|
||||||
|
|
||||||
|
def _on_screen_resume(self) -> None:
|
||||||
|
super()._on_screen_resume()
|
||||||
|
self.refresh_diaries()
|
||||||
|
|
||||||
def action_edit_selected_diary(self):
|
def action_edit_selected_diary(self):
|
||||||
"""Action to edit selected diary"""
|
"""Action to edit selected diary"""
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ from textual.widgets import Label, Input, Button
|
||||||
class EditDiaryModal(ModalScreen[tuple[int,str]]):
|
class EditDiaryModal(ModalScreen[tuple[int,str]]):
|
||||||
BINDINGS = [
|
BINDINGS = [
|
||||||
Binding("escape", "cancel", "Cancel"),
|
Binding("escape", "cancel", "Cancel"),
|
||||||
|
Binding("enter", "edit_diary", "Save",priority=True),
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, diary_id: int):
|
def __init__(self, diary_id: int):
|
||||||
|
|
@ -32,17 +33,28 @@ class EditDiaryModal(ModalScreen[tuple[int,str]]):
|
||||||
|
|
||||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
if event.button.id == "save_diary_button":
|
if event.button.id == "save_diary_button":
|
||||||
new_diary_name = self.name_input.value.strip()
|
self.action_edit_diary()
|
||||||
if new_diary_name and new_diary_name != self.current_diary_name:
|
|
||||||
self.dismiss((self.diary_id, new_diary_name))
|
|
||||||
elif new_diary_name == self.current_diary_name:
|
|
||||||
self.notify("No changes made.", severity="warning")
|
|
||||||
self.dismiss(None)
|
|
||||||
else:
|
|
||||||
self.notify("Diary name cannot be empty.", severity="warning")
|
|
||||||
self.name_input.focus()
|
|
||||||
elif event.button.id == "cancel_button":
|
elif event.button.id == "cancel_button":
|
||||||
self.dismiss(None)
|
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))
|
||||||
|
elif new_diary_name == self.current_diary_name:
|
||||||
|
self.notify("No changes made.", severity="warning")
|
||||||
|
self.dismiss(None)
|
||||||
|
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 == "edit_diary_name_input":
|
||||||
|
self.action_edit_diary()
|
||||||
|
|
||||||
def action_cancel(self) -> None:
|
def action_cancel(self) -> None:
|
||||||
self.dismiss(None)
|
self.dismiss(None)
|
||||||
|
|
@ -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"),
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,17 @@ from textual.containers import Vertical, Horizontal
|
||||||
from textual.screen import ModalScreen
|
from textual.screen import ModalScreen
|
||||||
from textual.widgets import Label, Input, Button
|
from textual.widgets import Label, Input, Button
|
||||||
|
|
||||||
|
from pilgrim.ui.screens.edit_entry_screen import EditEntryScreen
|
||||||
|
|
||||||
|
|
||||||
class NewDiaryModal(ModalScreen[str]):
|
class NewDiaryModal(ModalScreen[str]):
|
||||||
BINDINGS = [
|
BINDINGS = [
|
||||||
Binding("escape", "cancel", "Cancel"),
|
Binding("escape", "cancel", "Cancel"),
|
||||||
|
Binding("enter", "create_diary", "Create",priority=True),
|
||||||
]
|
]
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
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
|
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:
|
||||||
|
|
@ -31,15 +35,46 @@ class NewDiaryModal(ModalScreen[str]):
|
||||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
"""Handles button clicks."""
|
"""Handles button clicks."""
|
||||||
if event.button.id == "create_diary_button":
|
if event.button.id == "create_diary_button":
|
||||||
diary_name = self.name_input.value.strip()
|
self.action_create_diary()
|
||||||
if diary_name:
|
|
||||||
self.dismiss(diary_name)
|
|
||||||
else:
|
|
||||||
self.notify("Diary name cannot be empty.", severity="warning")
|
|
||||||
self.name_input.focus()
|
|
||||||
elif event.button.id == "cancel_button":
|
elif event.button.id == "cancel_button":
|
||||||
self.dismiss("")
|
self.dismiss()
|
||||||
|
|
||||||
def action_cancel(self) -> None:
|
def action_cancel(self) -> None:
|
||||||
"""Action to cancel the modal."""
|
"""Action to cancel the modal."""
|
||||||
self.dismiss("")
|
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)}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 */
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
from .directory_manager import DirectoryManager
|
from .directory_manager import DirectoryManager
|
||||||
|
from .config_manager import ConfigManager
|
||||||
|
|
||||||
__all__ = ['DirectoryManager']
|
__all__ = ['DirectoryManager', 'ConfigManager']
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue