mirror of
https://github.com/gmbrax/Pilgrim.git
synced 2026-11-10 09:30:17 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
335d06bca1 |
@@ -0,0 +1,24 @@
|
|||||||
|
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')
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
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
|
|
||||||
+30
-47
@@ -1,68 +1,51 @@
|
|||||||
# 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.
|
|
||||||
|
|
||||||
## Unreleased
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## Planned
|
## [Unreleased]
|
||||||
* Installation Method 1 (repository compilation)
|
|
||||||
* Organization of trips by date, location, or theme
|
|
||||||
* Enhanced photo management features
|
|
||||||
* Search functionality
|
|
||||||
* Export features
|
|
||||||
* Testing implementation
|
|
||||||
|
|
||||||
## [0.0.4] - 2025-07-19
|
### Planned
|
||||||
|
|
||||||
### Added
|
- Installation Method 1 (repository compilation)
|
||||||
* Support for creating new diaries asynchronously, with an option to automatically open the newly created diary
|
- Organization of trips by date, location, or theme
|
||||||
* Unified "Enter" key support for saving or creating diaries across relevant modals
|
- Enhanced photo management features
|
||||||
* Automatic diary list refresh when returning to the diary screen
|
- Search functionality
|
||||||
* Application configuration management with a new centralized config system
|
- Export features
|
||||||
* Database location and initialization now configurable via the new config manager
|
- Testing implementation
|
||||||
* 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
|
|
||||||
* Create and edit travel diaries
|
- Initial alpha release of Pilgrim travel diary application
|
||||||
* Create and edit diary entries
|
- Create and edit travel diaries
|
||||||
* Photo ingestion system
|
- Create and edit diary entries
|
||||||
* Photo addition and reference via sidebar
|
- Photo ingestion system
|
||||||
* Text User Interface (TUI) built with Textual framework
|
- Photo addition and reference via sidebar
|
||||||
* Pre-compiled binary installation method (Method 2)
|
- Text User Interface (TUI) built with Textual framework
|
||||||
* Support for Linux operating systems
|
- Pre-compiled binary installation method (Method 2)
|
||||||
* Basic project documentation (README)
|
- Support for Linux operating systems
|
||||||
|
- Basic project documentation (README)
|
||||||
|
|
||||||
### Known Issues
|
### Known Issues
|
||||||
* Installation Method 1 not yet implemented
|
|
||||||
* No testing suite implemented yet
|
- Installation Method 1 not yet implemented
|
||||||
* Some features may be unstable in an alpha version
|
- No testing suite implemented yet
|
||||||
|
- 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
|
||||||
|
|||||||
+1
-4
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "Pilgrim"
|
name = "Pilgrim"
|
||||||
version = "0.0.4"
|
version = "0.0.3"
|
||||||
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,9 +20,6 @@
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"sqlalchemy",
|
"sqlalchemy",
|
||||||
"textual",
|
"textual",
|
||||||
"tomli",
|
|
||||||
"tomli_w"
|
|
||||||
|
|
||||||
|
|
||||||
]
|
]
|
||||||
[template.plugins.default]
|
[template.plugins.default]
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
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 ConfigManager
|
from pilgrim.utils import DirectoryManager
|
||||||
|
|
||||||
|
|
||||||
class Application:
|
class Application:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.config_manager = ConfigManager()
|
self.config_dir = DirectoryManager.get_config_directory()
|
||||||
self.config_manager.read_config() # Chamar antes de criar o Database
|
self.database = 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.config_manager)
|
self.ui = UIApp(session_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()
|
||||||
|
|
||||||
|
|||||||
+26
-12
@@ -5,24 +5,38 @@ 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):
|
||||||
def __init__(self, config_manager: ConfigManager):
|
db_path = get_database_path()
|
||||||
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:///{self.db_path}",
|
f"sqlite:///{db_path}",
|
||||||
echo=False,
|
echo=False,
|
||||||
connect_args={"check_same_thread": False},
|
connect_args={"check_same_thread": False},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ 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
|
||||||
@@ -25,9 +24,6 @@ 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,9 +14,8 @@ class PhotoService:
|
|||||||
def __init__(self, session):
|
def __init__(self, session):
|
||||||
self.session = session
|
self.session = session
|
||||||
|
|
||||||
@staticmethod
|
def _hash_file(self, filepath: Path) -> str:
|
||||||
def hash_file(filepath: Path) -> str:
|
"""Calculate hash of a file using SHA3-384."""
|
||||||
"""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):
|
||||||
@@ -65,18 +64,10 @@ 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)
|
||||||
@@ -88,6 +79,8 @@ 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
|
||||||
@@ -125,7 +118,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(f"Version: {version('Pilgrim')}", id="AboutScreen_AboutVersion",
|
self.version = Label("Version: 0.0.1", 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",
|
||||||
|
|||||||
@@ -203,11 +203,17 @@ class DiaryListScreen(Screen):
|
|||||||
self.action_open_diary()
|
self.action_open_diary()
|
||||||
|
|
||||||
def action_new_diary(self):
|
def action_new_diary(self):
|
||||||
"""Action to create new diary"""
|
"""
|
||||||
|
Opens the modal dialog to create a new diary and registers a callback for submission.
|
||||||
|
"""
|
||||||
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):
|
||||||
"""Callback after diary creation"""
|
"""
|
||||||
|
Handles the callback after a new diary is submitted.
|
||||||
|
|
||||||
|
If a diary was created (non-empty result), notifies the user and refreshes the diary list; otherwise, notifies that creation was canceled.
|
||||||
|
"""
|
||||||
if result: # Se result não é string vazia, o diário foi criado
|
if result: # Se result não é string vazia, o diário foi criado
|
||||||
self.notify(f"Returning to diary list...")
|
self.notify(f"Returning to diary list...")
|
||||||
# Atualiza a lista de diários
|
# Atualiza a lista de diários
|
||||||
@@ -216,11 +222,17 @@ 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()
|
"""
|
||||||
|
Refreshes the diary list when the screen resumes.
|
||||||
|
"""
|
||||||
self.refresh_diaries()
|
self.refresh_diaries()
|
||||||
|
|
||||||
def action_edit_selected_diary(self):
|
def action_edit_selected_diary(self):
|
||||||
"""Action to edit selected diary"""
|
"""
|
||||||
|
Opens the edit dialog for the currently selected diary.
|
||||||
|
|
||||||
|
If a diary is selected, displays the edit modal and registers a callback for when the edit is submitted. Notifies the user if no diary is selected.
|
||||||
|
"""
|
||||||
if self.selected_diary_index is not None:
|
if self.selected_diary_index is not None:
|
||||||
diary_id = self.diary_id_map.get(self.selected_diary_index)
|
diary_id = self.diary_id_map.get(self.selected_diary_index)
|
||||||
if diary_id:
|
if diary_id:
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ class EditDiaryModal(ModalScreen[tuple[int,str]]):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, diary_id: int):
|
def __init__(self, diary_id: int):
|
||||||
|
"""
|
||||||
|
Initialize the EditDiaryModal with the specified diary ID, pre-filling the input field with the current diary name.
|
||||||
|
"""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.diary_id = diary_id
|
self.diary_id = diary_id
|
||||||
self.current_diary_name = self.app.service_manager.get_travel_diary_service().read_by_id(self.diary_id).name
|
self.current_diary_name = self.app.service_manager.get_travel_diary_service().read_by_id(self.diary_id).name
|
||||||
@@ -32,16 +35,29 @@ class EditDiaryModal(ModalScreen[tuple[int,str]]):
|
|||||||
self.name_input.cursor_position = len(self.name_input.value)
|
self.name_input.cursor_position = len(self.name_input.value)
|
||||||
|
|
||||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
|
"""
|
||||||
|
Handle button press events in the modal.
|
||||||
|
|
||||||
|
Triggers the save action if the "Save" button is pressed, or cancels and dismisses the modal if the "Cancel" button is pressed.
|
||||||
|
"""
|
||||||
if event.button.id == "save_diary_button":
|
if event.button.id == "save_diary_button":
|
||||||
self.action_edit_diary()
|
self.action_edit_diary()
|
||||||
elif event.button.id == "cancel_button":
|
elif event.button.id == "cancel_button":
|
||||||
self.dismiss(None)
|
self.dismiss(None)
|
||||||
def on_key(self, event):
|
def on_key(self, event):
|
||||||
|
"""
|
||||||
|
Handles the Enter key press event by triggering the diary save action and preventing the default behavior.
|
||||||
|
"""
|
||||||
if event.key == "enter":
|
if event.key == "enter":
|
||||||
self.action_edit_diary()
|
self.action_edit_diary()
|
||||||
event.prevent_default()
|
event.prevent_default()
|
||||||
|
|
||||||
def action_edit_diary(self) -> None:
|
def action_edit_diary(self) -> None:
|
||||||
|
"""
|
||||||
|
Attempts to save the edited diary name, validating input and providing user feedback.
|
||||||
|
|
||||||
|
If the new name is non-empty and different from the current name, dismisses the modal and returns the diary ID and new name. If the name is unchanged, notifies the user and dismisses without changes. If the input is empty, notifies the user and refocuses the input field.
|
||||||
|
"""
|
||||||
new_diary_name = self.name_input.value.strip()
|
new_diary_name = self.name_input.value.strip()
|
||||||
if new_diary_name and new_diary_name != self.current_diary_name:
|
if new_diary_name and new_diary_name != self.current_diary_name:
|
||||||
self.dismiss((self.diary_id, new_diary_name))
|
self.dismiss((self.diary_id, new_diary_name))
|
||||||
@@ -53,8 +69,14 @@ class EditDiaryModal(ModalScreen[tuple[int,str]]):
|
|||||||
self.name_input.focus()
|
self.name_input.focus()
|
||||||
|
|
||||||
def on_input_submitted(self, event: Input.Submitted) -> None:
|
def on_input_submitted(self, event: Input.Submitted) -> None:
|
||||||
|
"""
|
||||||
|
Handles submission of the diary name input field by triggering the save action if the relevant input is submitted.
|
||||||
|
"""
|
||||||
if event.input.id == "edit_diary_name_input":
|
if event.input.id == "edit_diary_name_input":
|
||||||
self.action_edit_diary()
|
self.action_edit_diary()
|
||||||
|
|
||||||
def action_cancel(self) -> None:
|
def action_cancel(self) -> None:
|
||||||
|
"""
|
||||||
|
Dismisses the modal without saving changes or returning any data.
|
||||||
|
"""
|
||||||
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,28 +296,30 @@ 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"
|
||||||
"\\[\\[photo::hash\\]\\]"
|
"Use: \\[\\[photo:name:hash\\]\\]\n"
|
||||||
|
"Or: \\[\\[photo::hash\\]\\]"
|
||||||
)
|
)
|
||||||
self.help_text.update(help_text)
|
self.help_text.update(help_text)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -427,7 +429,9 @@ 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)
|
||||||
@@ -611,8 +615,7 @@ 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)
|
||||||
@@ -739,15 +742,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"Name: {selected_photo.name}\n"
|
photo_details = f"📷 {selected_photo.name}\n"
|
||||||
photo_details += f"Hash: {photo_hash}\n"
|
photo_details += f"🔗 {photo_hash}\n"
|
||||||
photo_details += f"Date: {selected_photo.addition_date}\n"
|
photo_details += f"📅 {selected_photo.addition_date}\n"
|
||||||
if selected_photo.caption:
|
if selected_photo.caption:
|
||||||
photo_details += f"Caption: {selected_photo.caption}\n"
|
photo_details += f"💬 {selected_photo.caption}\n"
|
||||||
else:
|
photo_details += f"📁 {selected_photo.filepath}\n\n"
|
||||||
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::{photo_hash}]]"
|
photo_details += f"\\[\\[photo:{selected_photo.name}:{photo_hash}\\]\\]\n"
|
||||||
|
photo_details += f"\\[\\[photo::{photo_hash}\\]\\]"
|
||||||
|
|
||||||
self.photo_info.update(photo_details)
|
self.photo_info.update(photo_details)
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ 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(
|
||||||
@@ -63,16 +69,10 @@ 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,9 +82,13 @@ 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
|
||||||
@@ -93,7 +97,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": new_photo.photo_hash
|
"hash": 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,11 +12,15 @@ 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"),
|
||||||
@@ -41,9 +45,10 @@ class EditPhotoModal(Screen):
|
|||||||
id="caption-input",
|
id="caption-input",
|
||||||
classes="EditPhotoModal-Input"
|
classes="EditPhotoModal-Input"
|
||||||
),
|
),
|
||||||
Static(f"🔗 Photo Hash: {self.photo.photo_hash[:8]}", classes="EditPhotoModal-Hash"),
|
Static(f"🔗 Photo Hash: {photo_hash}", classes="EditPhotoModal-Hash"),
|
||||||
Static("Reference formats:", classes="EditPhotoModal-Label"),
|
Static("Reference formats:", classes="EditPhotoModal-Label"),
|
||||||
Static(f"\\[\\[photo::{self.photo.photo_hash[:8]}\\]\\]", classes="EditPhotoModal-Reference"),
|
Static(f"\\[\\[photo:{self.photo.name}:{photo_hash}\\]\\]", 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"),
|
||||||
|
|||||||
@@ -12,13 +12,25 @@ 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):
|
def __init__(self,autoopen=True):
|
||||||
|
"""
|
||||||
|
Initialize the NewDiaryModal with optional automatic opening of the created diary.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
autoopen (bool): If True, the newly created diary will open automatically after creation. Defaults to True.
|
||||||
|
"""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.auto_open = self.app.config_manager.auto_open_new_diary
|
self.auto_open = autoopen
|
||||||
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:
|
||||||
|
|
||||||
|
"""
|
||||||
|
Constructs and yields the UI layout for the new diary modal dialog.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ComposeResult: An iterable of widgets forming the modal, including title, input field, and action buttons.
|
||||||
|
"""
|
||||||
with Vertical(id="new_diary_dialog",classes="NewDiaryModal-Dialog"):
|
with Vertical(id="new_diary_dialog",classes="NewDiaryModal-Dialog"):
|
||||||
yield Label("Create a new diary", classes="NewDiaryModal-Title")
|
yield Label("Create a new diary", classes="NewDiaryModal-Title")
|
||||||
yield Label("Diary Name:")
|
yield Label("Diary Name:")
|
||||||
@@ -33,17 +45,28 @@ class NewDiaryModal(ModalScreen[str]):
|
|||||||
self.name_input.focus()
|
self.name_input.focus()
|
||||||
|
|
||||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
"""Handles button clicks."""
|
"""
|
||||||
|
Handle button press events for the modal.
|
||||||
|
|
||||||
|
Triggers diary creation when the "Create" button is pressed, or dismisses the modal when the "Cancel" button is pressed.
|
||||||
|
"""
|
||||||
if event.button.id == "create_diary_button":
|
if event.button.id == "create_diary_button":
|
||||||
self.action_create_diary()
|
self.action_create_diary()
|
||||||
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."""
|
"""
|
||||||
|
Cancels the modal dialog and dismisses it without returning a diary name.
|
||||||
|
"""
|
||||||
self.dismiss("")
|
self.dismiss("")
|
||||||
|
|
||||||
def action_create_diary(self) -> None:
|
def action_create_diary(self) -> None:
|
||||||
|
"""
|
||||||
|
Validates the diary name input and initiates asynchronous diary creation if valid.
|
||||||
|
|
||||||
|
If the input is empty, displays a warning notification and refocuses the input field.
|
||||||
|
"""
|
||||||
diary_name = self.name_input.value.strip()
|
diary_name = self.name_input.value.strip()
|
||||||
if diary_name:
|
if diary_name:
|
||||||
|
|
||||||
@@ -54,11 +77,19 @@ class NewDiaryModal(ModalScreen[str]):
|
|||||||
self.name_input.focus()
|
self.name_input.focus()
|
||||||
|
|
||||||
def on_input_submitted(self, event: Input.Submitted) -> None:
|
def on_input_submitted(self, event: Input.Submitted) -> None:
|
||||||
|
"""
|
||||||
|
Handles submission of the diary name input field and initiates diary creation if triggered from the correct input.
|
||||||
|
"""
|
||||||
if event.input.id == "NewDiaryModal-NameInput":
|
if event.input.id == "NewDiaryModal-NameInput":
|
||||||
self.action_create_diary()
|
self.action_create_diary()
|
||||||
|
|
||||||
async def _async_create_diary(self, name: str):
|
async def _async_create_diary(self, name: str):
|
||||||
|
|
||||||
|
"""
|
||||||
|
Asynchronously creates a new diary entry with the given name and handles post-creation actions.
|
||||||
|
|
||||||
|
If creation succeeds, dismisses the modal with the diary name, optionally opens the diary for editing, and shows a success notification. If creation fails or an exception occurs, displays an error notification.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
service = self.app.service_manager.get_travel_diary_service()
|
service = self.app.service_manager.get_travel_diary_service()
|
||||||
created_diary = await service.async_create(name)
|
created_diary = await service.async_create(name)
|
||||||
|
|||||||
@@ -389,10 +389,22 @@ Screen.-modal {
|
|||||||
width: 1fr;
|
width: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.EditEntryScreen-sidebar{
|
.EditEntryScreen-sidebar {
|
||||||
background: $primary-darken-1;
|
width: 40;
|
||||||
width: 45%;
|
min-height: 10;
|
||||||
|
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;
|
||||||
@@ -405,39 +417,30 @@ 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: 2fr; /* Pega o espaço restante disponível */
|
height: 1fr;
|
||||||
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: 1fr;
|
height: auto;
|
||||||
max-height: 15; /* Limita altura máxima */
|
min-height: 3;
|
||||||
min-height: 13;
|
|
||||||
padding: 1;
|
|
||||||
border: solid $warning;
|
border: solid $warning;
|
||||||
|
padding: 1;
|
||||||
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: 1fr;
|
height: auto;
|
||||||
max-height: 10; /* Altura máxima menor que info */
|
min-height: 8;
|
||||||
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,7 +9,6 @@ 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"
|
||||||
|
|
||||||
@@ -17,10 +16,9 @@ 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, config_manager: ConfigManager, **kwargs):
|
def __init__(self,service_manager: ServiceManager, **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,4 +1,3 @@
|
|||||||
from .directory_manager import DirectoryManager
|
from .directory_manager import DirectoryManager
|
||||||
from .config_manager import ConfigManager
|
|
||||||
|
|
||||||
__all__ = ['DirectoryManager', 'ConfigManager']
|
__all__ = ['DirectoryManager']
|
||||||
|
|||||||
@@ -1,107 +0,0 @@
|
|||||||
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,5 +1,4 @@
|
|||||||
import os
|
import os
|
||||||
import shutil
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
@@ -38,26 +37,3 @@ 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
|
|
||||||
|
|||||||
Reference in New Issue
Block a user