Compare commits

..
36 changed files with 884 additions and 222 deletions
+1 -143
View File
@@ -1,144 +1,2 @@
# Database files
database.db
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
build/
temp/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
Pipfile.lock
# poetry
poetry.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.env.*
.venv
venv/
ENV/
env/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# IDE settings
.vscode/
.idea/
__pycache__
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.10 (Pilgrim)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+6
View File
@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/Pilgrim.iml" filepath="$PROJECT_DIR$/.idea/Pilgrim.iml" />
</modules>
</component>
</project>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+200
View File
@@ -0,0 +1,200 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="0a7f92e2-b44a-4dfe-8e01-136d1c0c18be" name="Changes" comment="Added the travel_diary id as foreign key to the photos and add a check on the creation to avoid leaving it empty or bad referenced">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pilgrim/service/photo_service.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/pilgrim/service/photo_service.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pilgrim/ui/screens/diary_list_screen.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/pilgrim/ui/screens/diary_list_screen.py" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="Python Script" />
</list>
</option>
</component>
<component name="Git.Settings">
<option name="RECENT_BRANCH_BY_REPOSITORY">
<map>
<entry key="$PROJECT_DIR$" value="proposed_changes" />
</map>
</option>
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
<option name="UPDATE_TYPE" value="REBASE" />
</component>
<component name="GitHubPullRequestSearchHistory">{
&quot;lastFilter&quot;: {
&quot;state&quot;: &quot;OPEN&quot;,
&quot;assignee&quot;: &quot;gmbrax&quot;
}
}</component>
<component name="GithubPullRequestsUISettings">{
&quot;selectedUrlAndAccountId&quot;: {
&quot;url&quot;: &quot;https://github.com/gmbrax/Pilgrim.git&quot;,
&quot;accountId&quot;: &quot;213d8456-c67d-4cfd-99a6-337d47c35b4a&quot;
}
}</component>
<component name="ProjectColorInfo">{
&quot;associatedIndex&quot;: 0
}</component>
<component name="ProjectId" id="2y0y1J9RlHT6m1qL5RuCBf0Ramp" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"ModuleVcsDetector.initialDetectionPerformed": "true",
"Python.Database.executor": "Run",
"Python.command.executor": "Run",
"Python.main.executor": "Run",
"Python.pilgrim.executor": "Run",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.git.unshallow": "true",
"git-widget-placeholder": "feat-TUI",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
"vue.rearranger.settings.migration": "true"
}
}]]></component>
<component name="RunManager">
<configuration name="pilgrim" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
<module name="Pilgrim" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.venv/bin" />
<option name="IS_MODULE_SDK" value="false" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/.venv/bin/pilgrim" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
<recent_temporary>
<list>
<item itemvalue="Python.pilgrim" />
</list>
</recent_temporary>
</component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-js-predefined-d6986cc7102b-6a121458b545-JavaScript-PY-251.25410.159" />
<option value="bundled-python-sdk-e0ed3721d81e-36ea0e71a18c-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-251.25410.159" />
</set>
</attachedChunks>
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="0a7f92e2-b44a-4dfe-8e01-136d1c0c18be" name="Changes" comment="" />
<created>1748985568579</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1748985568579</updated>
<workItem from="1748985569621" duration="2124000" />
<workItem from="1748992451560" duration="312000" />
<workItem from="1749508687224" duration="371000" />
</task>
<task id="LOCAL-00001" summary="Added a Back Relationship in Entry to list all the photos">
<option name="closed" value="true" />
<created>1749004109515</created>
<option name="number" value="00001" />
<option name="presentableId" value="LOCAL-00001" />
<option name="project" value="LOCAL" />
<updated>1749004109515</updated>
</task>
<task id="LOCAL-00002" summary="Changed the names of the files to conform the python convection and also added photo_in_entry.py to diminish the cyclic import error in pylint">
<option name="closed" value="true" />
<created>1749006784623</created>
<option name="number" value="00002" />
<option name="presentableId" value="LOCAL-00002" />
<option name="project" value="LOCAL" />
<updated>1749006784623</updated>
</task>
<task id="LOCAL-00003" summary="Added the Services layer to the code to do the database operation">
<option name="closed" value="true" />
<created>1749140898576</created>
<option name="number" value="00003" />
<option name="presentableId" value="LOCAL-00003" />
<option name="project" value="LOCAL" />
<updated>1749140898576</updated>
</task>
<task id="LOCAL-00004" summary="Added the photo_service.py and modified photo.py to have all the crud operations">
<option name="closed" value="true" />
<created>1749155713848</created>
<option name="number" value="00004" />
<option name="presentableId" value="LOCAL-00004" />
<option name="project" value="LOCAL" />
<updated>1749155713848</updated>
</task>
<task id="LOCAL-00005" summary="Made some changes to naming in both the classes and the tables of the database to conform the correct naming scheme">
<option name="closed" value="true" />
<created>1749164385581</created>
<option name="number" value="00005" />
<option name="presentableId" value="LOCAL-00005" />
<option name="project" value="LOCAL" />
<updated>1749164385581</updated>
</task>
<task id="LOCAL-00006" summary="Added the travel_diary id as foreign key to the photos and add a check on the creation to avoid leaving it empty or bad referenced">
<option name="closed" value="true" />
<created>1749168650225</created>
<option name="number" value="00006" />
<option name="presentableId" value="LOCAL-00006" />
<option name="project" value="LOCAL" />
<updated>1749168650225</updated>
</task>
<option name="localTasksCounter" value="7" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="Vcs.Log.Tabs.Properties">
<option name="TAB_STATES">
<map>
<entry key="MAIN">
<value>
<State />
</value>
</entry>
</map>
</option>
</component>
<component name="VcsManagerConfiguration">
<MESSAGE value="Added a Back Relationship in Entry to list all the photos" />
<MESSAGE value="Changed the names of the files to conform the python convection and also added photo_in_entry.py to diminish the cyclic import error in pylint" />
<MESSAGE value="Added the Services layer to the code to do the database operation" />
<MESSAGE value="Added the photo_service.py and modified photo.py to have all the crud operations" />
<MESSAGE value="Made some changes to naming in both the classes and the tables of the database to conform the correct naming scheme" />
<MESSAGE value="Added the travel_diary id as foreign key to the photos and add a check on the creation to avoid leaving it empty or bad referenced" />
<option name="LAST_COMMIT_MESSAGE" value="Added the travel_diary id as foreign key to the photos and add a check on the creation to avoid leaving it empty or bad referenced" />
</component>
<component name="com.intellij.coverage.CoverageDataManagerImpl">
<SUITE FILE_PATH="coverage/Pilgrim$Database.coverage" NAME="Database Coverage Results" MODIFIED="1748987101492" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/src/Database" />
<SUITE FILE_PATH="coverage/Pilgrim$pilgrim.coverage" NAME="pilgrim Coverage Results" MODIFIED="1749097142827" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/.venv/bin" />
<SUITE FILE_PATH="coverage/Pilgrim$main.coverage" NAME="main Coverage Results" MODIFIED="1748992510527" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/src" />
<SUITE FILE_PATH="coverage/Pilgrim$command.coverage" NAME="command Coverage Results" MODIFIED="1748992876551" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/src/Pilgrim" />
</component>
</project>
+1 -48
View File
@@ -1,50 +1,3 @@
# Python_Pilgrim
## Overview
**Python_Pilgrim** is a Python-based travel diary application designed to help users document and manage their travel experiences. The project provides tools for recording trips, organizing travel notes, and storing memories in a structured and accessible format.
## Features
- Create and manage travel diaries
- Add, edit, and delete travel entries
- Organize trips by date, location, or theme
- Store photos, notes, and other media
- Export and share travel logs
## Requirements
- Python 3.8 or higher
- Linux operating system (tested on Ubuntu 20.04+)
- Visual Studio Code (VSCode) for development (optional but strongly recommended)
- pip (Python package installer)
- Optional: virtualenv for isolated environments
## Installation
1. Clone the repository:
```bash
git clone https://github.com/gmbrax/Pilgrim.git
```
2. Navigate to the project directory:
```bash
cd Pilgrim
```
3. Create a virtual environment and, then, activate it:
```bash
python -m venv .venv
source .venv/bin/activate
```
4. Install the required dependencies:
```bash
pip install -r requirements.txt
```
## Usage
To run the main application, execute:
```bash
python <???>.py
```
This will start the Python_Pilgrim application. Follow the on-screen instructions to create and manage your travel diaries.
Python Based Travel Diary
+3 -1
View File
@@ -18,7 +18,9 @@ classifiers = [
"Operating System :: OS Independent",
]
dependencies = [
"sqlalchemy"
"sqlalchemy",
"textual",
"textual-dev"
]
[template.plugins.default]
src-layout = true
+4 -1
View File
@@ -1,3 +1,6 @@
greenlet==3.2.3
greenlet==3.2.2
SQLAlchemy==2.0.41
typing_extensions==4.14.0
textual~=3.3.0
-6
View File
@@ -1,6 +0,0 @@
from src.application import Application
def main():
app = Application()
app.run()
+8
View File
@@ -0,0 +1,8 @@
from pilgrim.application import Application
from pilgrim.command import main
from pilgrim.database import Database, Base
from pilgrim.models.travel_diary import TravelDiary
from pilgrim.models.entry import Entry
from pilgrim.models.photo import Photo
__all__ = ["Application", "Database", "TravelDiary", "Entry", "Photo", "main", "Base"]
@@ -1,13 +1,17 @@
from src.database import Database
from src.service.servicemanager import ServiceManager
from pilgrim.database import Database
from pilgrim.service.mocks.service_manager_mock import ServiceManagerMock
from pilgrim.service.servicemanager import ServiceManager
from pilgrim.ui.ui import UIApp
class Application:
def __init__(self):
self.database = Database()
self.ui = UIApp(ServiceManagerMock())
def run(self):
self.database.create()
self.ui.run()
def get_service_manager(self):
session = self.database.session()
+9
View File
@@ -0,0 +1,9 @@
from pilgrim.application import Application
def main():
app = Application()
app.run()
if __name__ == "__main__":
main()
@@ -4,6 +4,7 @@ from sqlalchemy.orm import sessionmaker
Base = declarative_base()
class Database:
def __init__(self):
self.engine = create_engine(
@@ -3,8 +3,8 @@ from typing import Any
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
from src.models.photo_in_entry import photo_entry_association
from src.database import Base
from pilgrim.models.photo_in_entry import photo_entry_association
from ..database import Base
class Entry(Base):
@@ -1,10 +1,10 @@
from typing import Any
from sqlalchemy.orm import relationship
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
from src.models.photo_in_entry import photo_entry_association
from src.database import Base
from pilgrim.models.photo_in_entry import photo_entry_association
from ..database import Base
class Photo(Base):
@@ -1,6 +1,6 @@
from sqlalchemy import Table, Column, Integer, ForeignKey
from src.database import Base
from ..database import Base
photo_entry_association = Table('photo_entry_association', Base.metadata,
Column('id', Integer, primary_key=True, autoincrement=True),
@@ -1,9 +1,8 @@
from typing import Any
from src.database import Base
from sqlalchemy import Column, String, Integer
from ..database import Base
class TravelDiary(Base):
__tablename__ = "travel_diaries"
@@ -1,7 +1,7 @@
from typing import List
from src.models.entry import Entry
from src.models.travel_diary import TravelDiary
from ..models.entry import Entry
from ..models.travel_diary import TravelDiary
class EntryService:
@@ -0,0 +1,47 @@
from typing import List
from pilgrim.service.entry_service import EntryService
from pilgrim.models.entry import Entry
class EntryServiceMock(EntryService):
def __init__(self):
super().__init__(None)
self.mock_data = {
1: Entry(title="The Adventure Begins", text="I'm hopping in the Plane to finally visit canadian lands",
date="26/07/2025", travel_diary_id=1, id=1,
photos=[]),
2: Entry(title="The Landing", text="Finally on Canadian Soil", date="27/07/2025",
travel_diary_id=1, id=2,photos=[]),
3: Entry(title="The Mount Royal", text="The Mount Royal is fucking awesome", date="28/07/2025",
travel_diary_id=1, id=3, photos=[]),
}
self._next_id = 4
def create(self, travel_diary_id: int, title: str, text: str, date: str) -> Entry:
new_entry = Entry(title, text, date, travel_diary_id, id=self._next_id)
self.mock_data[self._next_id] = new_entry
self._next_id += 1
return new_entry
def read_by_id(self, entry_id: int) -> Entry | None:
return self.mock_data.get(entry_id)
def read_all(self) -> List[Entry]:
return list(self.mock_data.values())
def update(self, entry_id: int, entry_dst: Entry) -> Entry | None:
item_to_update = self.mock_data.get(entry_id)
if item_to_update:
item_to_update.title = entry_dst.title if entry_dst.title is not None else item_to_update.title
item_to_update.text = entry_dst.text if entry_dst.text is not None else item_to_update.text
item_to_update.date = entry_dst.date if entry_dst.date is not None else item_to_update.date
item_to_update.fk_travel_diary_id = entry_dst.fk_travel_diary_id if (entry_dst.fk_travel_diary_id
is not None) else entry_dst.id
item_to_update.photos.extend(entry_dst.photos)
return item_to_update
return None
def delete(self, entry_id: int) -> Entry | None:
return self.mock_data.pop(entry_id, None)
@@ -0,0 +1,42 @@
from pathlib import Path
from typing import List
from pilgrim.models.photo import Photo
from pilgrim.service.photo_service import PhotoService
class PhotoServiceMock(PhotoService):
def __init__(self):
super().__init__(None)
self.mock_data = {}
self._next_id = 1
def create(self, filepath: Path, name: str, travel_diary_id, addition_date=None, caption=None) -> Photo | None:
new_photo = Photo(filepath, name, addition_date=addition_date, caption=caption)
self.mock_data[self._next_id] = new_photo
self._next_id += 1
return new_photo
def read_by_id(self, photo_id: int) -> Photo:
return self.mock_data.get(photo_id)
def read_all(self) -> List[Photo]:
return list(self.mock_data.values())
def update(self, photo_id: Photo, photo_dst: Photo) -> Photo | None:
item_to_update:Photo = self.mock_data.get(photo_id)
if item_to_update:
item_to_update.filepath = photo_dst.filepath if photo_dst.filepath else item_to_update.filepath
item_to_update.name = photo_dst.name if photo_dst.name else item_to_update.name
item_to_update.caption = photo_dst.caption if photo_dst.caption else item_to_update.caption
item_to_update.addition_date = photo_dst.addition_date if photo_dst.addition_date\
else item_to_update.addition_date
item_to_update.fk_travel_diary_id = photo_dst.fk_travel_diary_id if photo_dst.fk_travel_diary_id \
else item_to_update.fk_travel_diary_id
item_to_update.entries.extend(photo_dst.entries)
return item_to_update
return None
def delete(self, photo_id: int) -> Photo | None:
return self.mock_data.pop(photo_id, None)
@@ -0,0 +1,23 @@
from pilgrim.service.mocks.entry_service_mock import EntryServiceMock
from pilgrim.service.mocks.photo_service_mock import PhotoServiceMock
from pilgrim.service.mocks.travel_diary_service_mock import TravelDiaryServiceMock
from pilgrim.service.photo_service import PhotoService
from pilgrim.service.servicemanager import ServiceManager
class ServiceManagerMock(ServiceManager):
def __init__(self):
super().__init__()
# Cria instâncias únicas para manter estado consistente
self._travel_diary_service = TravelDiaryServiceMock()
self._entry_service = EntryServiceMock()
self._photo_service = PhotoServiceMock()
def get_entry_service(self):
return self._entry_service
def get_travel_diary_service(self):
return self._travel_diary_service
def get_photo_service(self):
return self._photo_service
@@ -0,0 +1,67 @@
from pilgrim.service.travel_diary_service import TravelDiaryService
from pilgrim.models.travel_diary import TravelDiary
import asyncio
class TravelDiaryServiceMock(TravelDiaryService):
def __init__(self):
super().__init__(None)
self.mock_data = {
1: TravelDiary(id=1, name="Montreal"),
2: TravelDiary(id=2, name="Rio de Janeiro"),
}
self._next_id = 3
# Métodos síncronos (originais)
def create(self, name: str):
"""Versão síncrona"""
new_travel_diary = TravelDiary(id=self._next_id, name=name)
self.mock_data[self._next_id] = new_travel_diary
self._next_id += 1
return new_travel_diary
def read_by_id(self, travel_id: int):
"""Versão síncrona"""
return self.mock_data.get(travel_id)
def read_all(self):
"""Versão síncrona"""
return list(self.mock_data.values())
def update(self, travel_diary_id: int, name: str):
"""Versão síncrona"""
item_to_update = self.mock_data.get(travel_diary_id)
if item_to_update:
item_to_update.name = name
return item_to_update
return None
def delete(self, travel_diary_id: int):
"""Versão síncrona"""
return self.mock_data.pop(travel_diary_id, None)
# Métodos assíncronos (novos)
async def async_create(self, name: str):
"""Versão assíncrona"""
await asyncio.sleep(0.01) # Simula I/O
return self.create(name)
async def async_read_by_id(self, travel_id: int):
"""Versão assíncrona"""
await asyncio.sleep(0.01) # Simula I/O
return self.read_by_id(travel_id)
async def async_read_all(self):
"""Versão assíncrona"""
await asyncio.sleep(0.01) # Simula I/O
return self.read_all()
async def async_update(self, travel_diary_id: int, name: str):
"""Versão assíncrona"""
await asyncio.sleep(0.01) # Simula I/O
return self.update(travel_diary_id, name)
async def async_delete(self, travel_diary_id: int):
"""Versão assíncrona"""
await asyncio.sleep(0.01) # Simula I/O
return self.delete(travel_diary_id)
@@ -1,9 +1,9 @@
from pathlib import Path
from typing import List
from src.models.photo import Photo
from src.models.travel_diary import TravelDiary
from pilgrim.models.photo import Photo
from pilgrim.models.travel_diary import TravelDiary
class PhotoService:
def __init__(self, session):
@@ -1,5 +1,5 @@
from src.service.entry_service import EntryService
from src.service.travel_diary_service import TravelDiaryService
from pilgrim.service.entry_service import EntryService
from pilgrim.service.travel_diary_service import TravelDiaryService
class ServiceManager:
@@ -1,4 +1,4 @@
from src.models.travel_diary import TravelDiary
from ..models.travel_diary import TravelDiary
class TravelDiaryService:
@@ -18,18 +18,18 @@ class TravelDiaryService:
def read_all(self):
return self.session.query(TravelDiary).all()
def update(self, travel_diary_src:TravelDiary,travel_diary_dst:TravelDiary):
original = self.read_by_id(travel_diary_src.id)
def update(self, travel_diary_id: TravelDiary, travel_diary_dst: TravelDiary):
original = self.read_by_id(travel_diary_id.id)
if original is not None:
original.name = travel_diary_dst.name
self.session.commit()
self.session.refresh(original)
return original
def delete(self, travel_diary_src:TravelDiary):
excluded = self.read_by_id(travel_diary_src.id)
def delete(self, travel_diary_id: TravelDiary):
excluded = self.read_by_id(travel_diary_id.id)
if excluded is not None:
self.session.delete(travel_diary_src)
self.session.delete(travel_diary_id)
self.session.commit()
return excluded
return None
View File
View File
+286
View File
@@ -0,0 +1,286 @@
from typing import Optional, Tuple
import asyncio
from textual.app import ComposeResult
from textual.screen import Screen
from textual.widgets import Header, Footer, Label, Static, OptionList, Button
from textual.binding import Binding
from textual.containers import Vertical, Container, Horizontal
from pilgrim.models.travel_diary import TravelDiary
from pilgrim.ui.screens.edit_diary_modal import EditDiaryModal
from pilgrim.ui.screens.new_diary_modal import NewDiaryModal
class DiaryListScreen(Screen):
TITLE = "Pilgrim - Main"
BINDINGS = [
Binding("n", "new_diary", "New diary"),
Binding("^q", "quit", "Quit Pilgrim"),
Binding("enter", "open_selected_diary", "Open diary"),
Binding("e", "edit_selected_diary", "Edit diary"),
Binding("r", "force_refresh", "Force refresh"),
]
def __init__(self):
super().__init__()
self.selected_diary_index = None
self.diary_id_map = {}
self.is_refreshing = False
self.header = Header()
self.footer = Footer()
self.diary_list = OptionList(classes="DiaryListScreen-DiaryListOptions")
self.new_diary_button = Button("New diary", id="new_diary", classes="DiaryListScreen-NewDiaryButton")
self.edit_diary_button = Button("Edit diary", id="edit_diary", classes="DiaryListScreen-EditDiaryButton")
self.open_diary = Button("Open diary", id="open_diary", classes="DiaryListScreen-OpenDiaryButton")
self.buttons_grid = Horizontal(
self.new_diary_button, self.edit_diary_button, self.open_diary,
classes="DiaryListScreen-ButtonsGrid"
)
self.tips = Static(
"Tip: use ↑↓ to navigate • ENTER to Select • "
"TAB to alternate the fields • SHIFT + TAB to alternate back • "
"Ctrl+P for command palette • R to force refresh",
classes="DiaryListScreen-DiaryListTips"
)
self.container = Container(
self.diary_list, self.buttons_grid, self.tips,
classes="DiaryListScreen-DiaryListContainer"
)
def compose(self) -> ComposeResult:
yield self.header
yield self.container
yield self.footer
def on_mount(self) -> None:
# Usa versão síncrona para o mount inicial
self.refresh_diaries()
self.update_buttons_state()
def refresh_diaries(self):
"""Versão síncrona do refresh"""
try:
service_manager = self.app.service_manager
travel_diary_service = service_manager.get_travel_diary_service()
# Usa método síncrono
diaries = travel_diary_service.read_all()
# Salva o estado atual
current_diary_id = None
if (self.selected_diary_index is not None and
self.selected_diary_index in self.diary_id_map):
current_diary_id = self.diary_id_map[self.selected_diary_index]
# Limpa e reconstrói
self.diary_list.clear_options()
self.diary_id_map = {}
if not diaries:
self.diary_list.add_option("[dim]Nenhum diário encontrado. Pressione 'N' para criar um novo![/dim]")
self.selected_diary_index = None
else:
new_selected_index = 0
for index, diary in enumerate(diaries):
self.diary_id_map[index] = diary.id
self.diary_list.add_option(f"[b]{diary.name}[/b]\n[dim]ID: {diary.id}[/dim]")
# Mantém a seleção se possível
if current_diary_id and diary.id == current_diary_id:
new_selected_index = index
self.selected_diary_index = new_selected_index
# Atualiza o highlight
self.set_timer(0.05, lambda: self._update_highlight(new_selected_index))
# Força refresh visual
self.diary_list.refresh()
self.update_buttons_state()
except Exception as e:
self.notify(f"Erro ao carregar diários: {str(e)}")
def _update_highlight(self, index: int):
"""Atualiza o highlight do OptionList"""
try:
if index < len(self.diary_list.options):
self.diary_list.highlighted = index
self.diary_list.refresh()
except Exception as e:
self.notify(f"Erro ao atualizar highlight: {str(e)}")
async def async_refresh_diaries(self):
"""Versão assíncrona do refresh"""
if self.is_refreshing:
return
self.is_refreshing = True
try:
service_manager = self.app.service_manager
travel_diary_service = service_manager.get_travel_diary_service()
# Usa método assíncrono
diaries = await travel_diary_service.async_read_all()
# Salva o estado atual
current_diary_id = None
if (self.selected_diary_index is not None and
self.selected_diary_index in self.diary_id_map):
current_diary_id = self.diary_id_map[self.selected_diary_index]
# Limpa e reconstrói
self.diary_list.clear_options()
self.diary_id_map = {}
if not diaries:
self.diary_list.add_option("[dim]Nenhum diário encontrado. Pressione 'N' para criar um novo![/dim]")
self.selected_diary_index = None
else:
new_selected_index = 0
for index, diary in enumerate(diaries):
self.diary_id_map[index] = diary.id
self.diary_list.add_option(f"[b]{diary.name}[/b]\n[dim]ID: {diary.id}[/dim]")
if current_diary_id and diary.id == current_diary_id:
new_selected_index = index
self.selected_diary_index = new_selected_index
self.set_timer(0.05, lambda: self._update_highlight(new_selected_index))
self.diary_list.refresh()
self.update_buttons_state()
except Exception as e:
self.notify(f"Erro ao carregar diários: {str(e)}")
finally:
self.is_refreshing = False
def on_option_list_option_highlighted(self, event: OptionList.OptionHighlighted) -> None:
"""Handle quando uma opção é destacada"""
if self.diary_id_map and event.option_index in self.diary_id_map:
self.selected_diary_index = event.option_index
else:
self.selected_diary_index = None
self.update_buttons_state()
def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None:
"""Handle quando uma opção é selecionada"""
if self.diary_id_map and event.option_index in self.diary_id_map:
self.selected_diary_index = event.option_index
self.action_open_diary()
else:
self.selected_diary_index = None
self.update_buttons_state()
def update_buttons_state(self):
"""Atualiza o estado dos botões"""
has_selection = (self.selected_diary_index is not None and
self.selected_diary_index in self.diary_id_map)
self.edit_diary_button.disabled = not has_selection
self.open_diary.disabled = not has_selection
def on_button_pressed(self, event: Button.Pressed) -> None:
"""Handle cliques nos botões"""
button_id = event.button.id
if button_id == "new_diary":
self.action_new_diary()
elif button_id == "edit_diary":
self.action_edit_selected_diary()
elif button_id == "open_diary":
self.action_open_diary()
def action_new_diary(self):
"""Ação para criar novo diário"""
self.app.push_screen(NewDiaryModal(),self._on_new_diary_submitted)
def _on_new_diary_submitted(self,result):
self.notify(str(result))
if result:
self.notify(f"Creating Diary:{result}'...")
self.call_later(self._async_create_diary,result)
else:
self.notify(f"Canceled...")
async def _async_create_diary(self,name: str):
try:
service = self.app.service_manager.get_travel_diary_service()
created_diary = await service.async_create(name)
if created_diary:
self.diary_id_map[created_diary.id] = created_diary.id
await self.async_refresh_diaries()
self.notify(f"Diary: '{name}' created!")
else:
self.notify("Error Creating the diary")
except Exception as e:
self.notify(f"Exception on creating the diary: {str(e)}")
def action_edit_selected_diary(self):
"""Ação para editar diário selecionado"""
if self.selected_diary_index is not None:
diary_id = self.diary_id_map.get(self.selected_diary_index)
if diary_id:
self.app.push_screen(
EditDiaryModal(diary_id=diary_id),
self._on_edited_diary_name_submitted
)
else:
self.notify("Selecione um diário para editar")
def action_open_diary(self):
"""Ação para abrir diário selecionado"""
if self.selected_diary_index is not None:
diary_id = self.diary_id_map.get(self.selected_diary_index)
self.notify(f"Abrindo diário ID: {diary_id}")
else:
self.notify("Selecione um diário para abrir")
def _on_edited_diary_name_submitted(self, result: Optional[Tuple[int, str]]) -> None:
"""Callback após edição do diário"""
if result:
diary_id, name = result
self.notify(f"Atualizando diário ID {diary_id} para '{name}'...")
# Agenda a atualização assíncrona
self.call_later(self._async_update_diary, diary_id, name)
else:
self.notify("Edição cancelada")
async def _async_update_diary(self, diary_id: int, name: str):
"""Atualiza o diário de forma assíncrona"""
try:
service = self.app.service_manager.get_travel_diary_service()
updated_diary = await service.async_update(diary_id, name)
if updated_diary:
self.notify(f"Diário '{name}' atualizado!")
# Força refresh após a atualização
await self.async_refresh_diaries()
else:
self.notify("Erro: Diário não encontrado")
except Exception as e:
self.notify(f"Erro ao atualizar: {str(e)}")
def action_force_refresh(self):
"""Força refresh manual"""
self.notify("Forçando refresh...")
# Tenta ambas as versões
self.refresh_diaries() # Síncrona
self.call_later(self.async_refresh_diaries) # Assíncrona
def action_open_selected_diary(self):
"""Ação do binding ENTER"""
self.action_open_diary()
@@ -0,0 +1,48 @@
from textual.app import ComposeResult
from textual.containers import Vertical, Horizontal
from textual.screen import ModalScreen
from textual.widgets import Label, Input, Button
class EditDiaryModal(ModalScreen[tuple[int,str]]):
BINDINGS = [
("escape", "cancel", "Cancelar"),
]
def __init__(self, diary_id: int):
super().__init__()
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.name_input = Input(value=self.current_diary_name, id="edit_diary_name_input")
def compose(self) -> ComposeResult:
with Vertical(id="edit_diary_dialog"):
yield Label(f"Editar Diário: {self.current_diary_name}", classes="dialog-title")
yield Label("Novo Nome do Diário:")
yield self.name_input
with Horizontal(classes="dialog-buttons"):
yield Button("Salvar", variant="primary", id="save_diary_button")
yield Button("Cancelar", variant="default", id="cancel_button")
def on_mount(self) -> None:
"""Foca no campo de entrada e move o cursor para o final do texto."""
self.name_input.focus()
self.name_input.cursor_position = len(self.name_input.value)
# REMOVIDA A LINHA QUE CAUSA O ERRO: self.name_input.select_text()
def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "save_diary_button":
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("Nenhuma alteração feita.", severity="warning")
self.dismiss(None)
else:
self.notify("O nome do diário não pode estar vazio.", severity="warning")
self.name_input.focus()
elif event.button.id == "cancel_button":
self.dismiss(None)
def action_cancel(self) -> None:
self.dismiss(None)
+41
View File
@@ -0,0 +1,41 @@
from textual.app import ComposeResult
from textual.containers import Vertical, Horizontal
from textual.screen import ModalScreen
from textual.widgets import Label, Input, Button
class NewDiaryModal(ModalScreen[str]):
BINDINGS = [
("escape", "cancel", "Cancelar"),
]
def __init__(self):
super().__init__()
self.name_input = Input(id="NewDiaryModal-NameInput") # This ID is fine, it's specific to the input
def compose(self) -> ComposeResult:
# CHANGE THIS LINE: Use the ID that matches your CSS
with Vertical(id="new_diary_dialog"): # <--- Changed ID here to match CSS
yield Label("Create a new diary", classes="dialog-title")
yield Label("Diary Name:")
yield self.name_input
with Horizontal(classes="dialog-buttons"):
yield Button("Create", variant="primary", id="create_diary_button")
yield Button("Cancel", variant="default", id="cancel_button")
def on_mount(self):
self.name_input.focus()
def on_button_pressed(self, event: Button.Pressed) -> None:
"""Lida com os cliques dos botões."""
if event.button.id == "create_diary_button":
diary_name = self.name_input.value.strip()
if diary_name:
self.dismiss(diary_name)
else:
self.notify("O nome do diário não pode estar vazio.", severity="warning")
self.name_input.focus()
elif event.button.id == "cancel_button":
self.dismiss("")
def action_cancel(self) -> None:
"""Ação para cancelar a modal."""
self.dismiss("")
+25
View File
@@ -0,0 +1,25 @@
.dialog-container{
content-align: center top;
}
.app-title{
dock: top;
height: 3;
content-align: center middle;
text-style: bold;
color: $accent;
}
.actions-buttons{
height: auto;
margin: 1 0;
align: center middle;
}
.tips{
dock: bottom;
height: 4;
content-align: center middle;
color: $text-muted;
}
+21
View File
@@ -0,0 +1,21 @@
from pathlib import Path
from textual.app import App
from pilgrim.service.servicemanager import ServiceManager
from pilgrim.ui.screens.diary_list_screen import DiaryListScreen
CSS_FILE_PATH = Path(__file__).parent / "styles" / "pilgrim.css"
class UIApp(App):
CSS_PATH = CSS_FILE_PATH
def __init__(self,service_manager: ServiceManager):
super().__init__()
self.service_manager = service_manager
def on_mount(self) -> None:
"""Chamado quando a app inicia. Carrega a tela principal."""
self.push_screen(DiaryListScreen())