mirror of https://github.com/gmbrax/Pilgrim.git
Merge pull request #13 from gmbrax/feat/photo-sidebar-tui
Feat/photo sidebar tui
This commit is contained in:
commit
2973620573
|
|
@ -1,3 +1,146 @@
|
|||
# Database files
|
||||
database.db
|
||||
__pycache__
|
||||
/.idea/
|
||||
|
||||
.build-vend/
|
||||
dist_nuitka/
|
||||
# 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/
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
<?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>
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
<?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>
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -1,200 +0,0 @@
|
|||
<?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">{
|
||||
"lastFilter": {
|
||||
"state": "OPEN",
|
||||
"assignee": "gmbrax"
|
||||
}
|
||||
}</component>
|
||||
<component name="GithubPullRequestsUISettings">{
|
||||
"selectedUrlAndAccountId": {
|
||||
"url": "https://github.com/gmbrax/Pilgrim.git",
|
||||
"accountId": "213d8456-c67d-4cfd-99a6-337d47c35b4a"
|
||||
}
|
||||
}</component>
|
||||
<component name="ProjectColorInfo">{
|
||||
"associatedIndex": 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>
|
||||
49
README.md
49
README.md
|
|
@ -1,3 +1,50 @@
|
|||
# Python_Pilgrim
|
||||
|
||||
Python Based Travel Diary
|
||||
## 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.
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -12,10 +12,13 @@ class Database:
|
|||
echo=False,
|
||||
connect_args={"check_same_thread": False},
|
||||
)
|
||||
self.session = sessionmaker(bind=self.engine, autoflush=False, autocommit=False)
|
||||
self._session_maker = sessionmaker(bind=self.engine, autoflush=False, autocommit=False)
|
||||
|
||||
def create(self):
|
||||
Base.metadata.create_all(self.engine)
|
||||
|
||||
def session(self):
|
||||
return self._session_maker()
|
||||
|
||||
def get_db(self):
|
||||
return self.session()
|
||||
return self._session_maker()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
from typing import Any
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, DateTime
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from pilgrim.models.photo_in_entry import photo_entry_association
|
||||
|
|
@ -12,7 +14,7 @@ class Photo(Base):
|
|||
id = Column(Integer, primary_key=True)
|
||||
filepath = Column(String)
|
||||
name = Column(String)
|
||||
addition_date = Column(String)
|
||||
addition_date = Column(DateTime, default=datetime.now)
|
||||
caption = Column(String)
|
||||
entries = relationship(
|
||||
"Entry",
|
||||
|
|
@ -22,10 +24,16 @@ class Photo(Base):
|
|||
|
||||
fk_travel_diary_id = Column(Integer, ForeignKey("travel_diaries.id"),nullable=False)
|
||||
|
||||
def __init__(self, filepath, name, addition_date=None, caption=None, entries=None, **kw: Any):
|
||||
def __init__(self, filepath, name, addition_date=None, caption=None, entries=None, fk_travel_diary_id=None, **kw: Any):
|
||||
super().__init__(**kw)
|
||||
self.filepath = filepath
|
||||
# Convert Path to string if needed
|
||||
if isinstance(filepath, Path):
|
||||
self.filepath = str(filepath)
|
||||
else:
|
||||
self.filepath = filepath
|
||||
self.name = name
|
||||
self.addition_date = addition_date
|
||||
self.addition_date = addition_date if addition_date is not None else datetime.now()
|
||||
self.caption = caption
|
||||
self.entries = entries
|
||||
self.entries = entries if entries is not None else []
|
||||
if fk_travel_diary_id is not None:
|
||||
self.fk_travel_diary_id = fk_travel_diary_id
|
||||
|
|
|
|||
|
|
@ -12,7 +12,14 @@ class PhotoServiceMock(PhotoService):
|
|||
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)
|
||||
new_photo = Photo(
|
||||
filepath=filepath,
|
||||
name=name,
|
||||
addition_date=addition_date,
|
||||
caption=caption,
|
||||
fk_travel_diary_id=travel_diary_id
|
||||
)
|
||||
new_photo.id = self._next_id
|
||||
self.mock_data[self._next_id] = new_photo
|
||||
self._next_id += 1
|
||||
return new_photo
|
||||
|
|
@ -24,19 +31,18 @@ class PhotoServiceMock(PhotoService):
|
|||
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)
|
||||
def update(self, photo_src: Photo, photo_dst: Photo) -> Photo | None:
|
||||
item_to_update: Photo = self.mock_data.get(photo_src.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)
|
||||
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
|
||||
if photo_dst.entries:
|
||||
item_to_update.entries = 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)
|
||||
def delete(self, photo_src: Photo) -> Photo | None:
|
||||
return self.mock_data.pop(photo_src.id, None)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from pathlib import Path
|
||||
from typing import List
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
from pilgrim.models.photo import Photo
|
||||
|
|
@ -9,11 +10,25 @@ class PhotoService:
|
|||
def __init__(self, session):
|
||||
self.session = session
|
||||
|
||||
def create(self, filepath:Path, name:str, travel_diary_id, addition_date=None, caption=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()
|
||||
if not travel_diary:
|
||||
return None
|
||||
new_photo = Photo(filepath, name, addition_date=addition_date, caption=caption)
|
||||
|
||||
# Convert addition_date string to datetime if needed
|
||||
if isinstance(addition_date, str):
|
||||
try:
|
||||
addition_date = datetime.strptime(addition_date, "%Y-%m-%d %H:%M:%S")
|
||||
except ValueError:
|
||||
addition_date = None
|
||||
|
||||
new_photo = Photo(
|
||||
filepath=filepath,
|
||||
name=name,
|
||||
caption=caption,
|
||||
fk_travel_diary_id=travel_diary_id,
|
||||
addition_date=addition_date
|
||||
)
|
||||
self.session.add(new_photo)
|
||||
self.session.commit()
|
||||
self.session.refresh(new_photo)
|
||||
|
|
@ -25,24 +40,37 @@ class PhotoService:
|
|||
def read_all(self) -> List[Photo]:
|
||||
return self.session.query(Photo).all()
|
||||
|
||||
def update(self,photo_src:Photo,photo_dst:Photo) -> Photo | None:
|
||||
original:Photo = self.read_by_id(photo_src.id)
|
||||
def update(self, photo_src: Photo, photo_dst: Photo) -> Photo | None:
|
||||
original: Photo = self.read_by_id(photo_src.id)
|
||||
if original:
|
||||
original.filepath = photo_dst.filepath
|
||||
original.name = photo_dst.name
|
||||
original.addition_date = photo_dst.addition_date
|
||||
original.caption = photo_dst.caption
|
||||
original.entries.extend(photo_dst.entries)
|
||||
if photo_dst.entries and len(photo_dst.entries) > 0:
|
||||
if original.entries is None:
|
||||
original.entries = []
|
||||
original.entries = photo_dst.entries # Replace instead of extend
|
||||
self.session.commit()
|
||||
self.session.refresh(original)
|
||||
return original
|
||||
return None
|
||||
|
||||
def delete(self, photo_src:Photo) -> Photo | None:
|
||||
def delete(self, photo_src: Photo) -> Photo | None:
|
||||
excluded = self.read_by_id(photo_src.id)
|
||||
if excluded:
|
||||
# Store photo data before deletion
|
||||
deleted_photo = Photo(
|
||||
filepath=excluded.filepath,
|
||||
name=excluded.name,
|
||||
addition_date=excluded.addition_date,
|
||||
caption=excluded.caption,
|
||||
fk_travel_diary_id=excluded.fk_travel_diary_id,
|
||||
id=excluded.id
|
||||
)
|
||||
|
||||
self.session.delete(excluded)
|
||||
self.session.commit()
|
||||
self.session.refresh(excluded)
|
||||
return excluded
|
||||
|
||||
return deleted_photo
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from pilgrim.service.entry_service import EntryService
|
||||
from pilgrim.service.photo_service import PhotoService
|
||||
from pilgrim.service.travel_diary_service import TravelDiaryService
|
||||
|
||||
|
||||
|
|
@ -17,3 +18,7 @@ class ServiceManager:
|
|||
if self.session is not None:
|
||||
return TravelDiaryService(self.session)
|
||||
return None
|
||||
def get_photo_service(self):
|
||||
if self.session is not None:
|
||||
return PhotoService(self.session)
|
||||
return None
|
||||
|
|
@ -1,15 +1,21 @@
|
|||
from typing import Optional, List
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from textual.app import ComposeResult
|
||||
from textual.screen import Screen
|
||||
from textual.widgets import Header, Footer, Static, TextArea
|
||||
from textual.widgets import Header, Footer, Static, TextArea, OptionList, Input, Button
|
||||
from textual.binding import Binding
|
||||
from textual.containers import Container, Horizontal
|
||||
from textual.containers import Container, Horizontal, Vertical, ScrollableContainer
|
||||
|
||||
from pilgrim.models.entry import Entry
|
||||
from pilgrim.models.travel_diary import TravelDiary
|
||||
from pilgrim.models.photo import Photo
|
||||
from pilgrim.ui.screens.modals.add_photo_modal import AddPhotoModal
|
||||
from pilgrim.ui.screens.modals.edit_photo_modal import EditPhotoModal
|
||||
from pilgrim.ui.screens.modals.confirm_delete_modal import ConfirmDeleteModal
|
||||
from pilgrim.ui.screens.modals.file_picker_modal import FilePickerModal
|
||||
from pilgrim.ui.screens.rename_entry_modal import RenameEntryModal
|
||||
|
||||
|
||||
|
|
@ -21,13 +27,16 @@ class EditEntryScreen(Screen):
|
|||
Binding("ctrl+n", "next_entry", "Next/New Entry"),
|
||||
Binding("ctrl+b", "prev_entry", "Previous Entry"),
|
||||
Binding("ctrl+r", "rename_entry", "Rename Entry"),
|
||||
Binding("escape", "back_to_list", "Back to List")
|
||||
Binding("escape", "back_to_list", "Back to List"),
|
||||
Binding("f8", "toggle_sidebar", "Toggle Sidebar"),
|
||||
Binding("f9", "toggle_focus", "Focus Sidebar/Editor"),
|
||||
]
|
||||
|
||||
def __init__(self, diary_id: int = 1):
|
||||
print("DEBUG: EditEntryScreen INIT")
|
||||
super().__init__()
|
||||
self.diary_id = diary_id
|
||||
self.diary_name = f"Diary {diary_id}" # Use a better default name
|
||||
self.diary_name = f"Diary {diary_id}"
|
||||
self.current_entry_index = 0
|
||||
self.entries: List[Entry] = []
|
||||
self.is_new_entry = False
|
||||
|
|
@ -38,6 +47,9 @@ class EditEntryScreen(Screen):
|
|||
self._updating_display = False
|
||||
self._original_content = ""
|
||||
self.is_refreshing = False
|
||||
self.sidebar_visible = False
|
||||
self.sidebar_focused = False
|
||||
self._sidebar_opened_once = False
|
||||
|
||||
# Main header
|
||||
self.header = Header(name="Pilgrim v6", classes="EditEntryScreen-header")
|
||||
|
|
@ -60,6 +72,27 @@ class EditEntryScreen(Screen):
|
|||
# Text area
|
||||
self.text_entry = TextArea(id="text_entry", classes="EditEntryScreen-text-entry")
|
||||
|
||||
# Sidebar widgets
|
||||
self.sidebar_title = Static("📸 Photos", classes="EditEntryScreen-sidebar-title")
|
||||
self.photo_list = OptionList(id="photo_list", classes="EditEntryScreen-sidebar-photo-list")
|
||||
self.photo_info = Static("", classes="EditEntryScreen-sidebar-photo-info")
|
||||
self.help_text = Static("", classes="EditEntryScreen-sidebar-help")
|
||||
|
||||
# Sidebar container: photo list and info in a flexible container, help_text fixed at bottom
|
||||
self.sidebar_content = Vertical(
|
||||
self.photo_list,
|
||||
self.photo_info,
|
||||
id="sidebar_content",
|
||||
classes="EditEntryScreen-sidebar-content"
|
||||
)
|
||||
self.sidebar = Vertical(
|
||||
self.sidebar_title,
|
||||
self.sidebar_content,
|
||||
self.help_text, # Always at the bottom, never scrolls
|
||||
id="sidebar",
|
||||
classes="EditEntryScreen-sidebar"
|
||||
)
|
||||
|
||||
# Main container
|
||||
self.main = Container(
|
||||
self.sub_header,
|
||||
|
|
@ -71,16 +104,32 @@ class EditEntryScreen(Screen):
|
|||
# Footer
|
||||
self.footer = Footer(classes="EditEntryScreen-footer")
|
||||
|
||||
def _update_footer_context(self):
|
||||
"""Forces footer refresh to show updated bindings"""
|
||||
self.refresh()
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
print("DEBUG: EditEntryScreen COMPOSE", getattr(self, 'sidebar_visible', None))
|
||||
yield self.header
|
||||
yield self.main
|
||||
yield Horizontal(
|
||||
self.main,
|
||||
self.sidebar,
|
||||
id="content_container",
|
||||
classes="EditEntryScreen-content-container"
|
||||
)
|
||||
yield self.footer
|
||||
|
||||
def on_mount(self) -> None:
|
||||
"""Called when the screen is mounted"""
|
||||
self.sidebar.display = False
|
||||
self.sidebar_visible = False
|
||||
|
||||
# First update diary info, then refresh entries
|
||||
self.update_diary_info()
|
||||
self.refresh_entries()
|
||||
|
||||
# Initialize footer with editor context
|
||||
self._update_footer_context()
|
||||
|
||||
def update_diary_info(self):
|
||||
"""Updates diary information"""
|
||||
|
|
@ -93,17 +142,14 @@ class EditEntryScreen(Screen):
|
|||
self.diary_name = diary.name
|
||||
self.diary_info.update(f"Diary: {self.diary_name}")
|
||||
else:
|
||||
# If diary not found, try to get a default name
|
||||
self.diary_name = f"Diary {self.diary_id}"
|
||||
self.diary_info.update(f"Diary: {self.diary_name}")
|
||||
self.notify(f"Diary {self.diary_id} not found, using default name")
|
||||
except Exception as e:
|
||||
# If there's an error, use a default name but don't break the app
|
||||
self.diary_name = f"Diary {self.diary_id}"
|
||||
self.diary_info.update(f"Diary: {self.diary_name}")
|
||||
self.notify(f"Error loading diary info: {str(e)}")
|
||||
|
||||
# Always ensure the diary info is updated
|
||||
self._ensure_diary_info_updated()
|
||||
|
||||
def _ensure_diary_info_updated(self):
|
||||
|
|
@ -111,7 +157,6 @@ class EditEntryScreen(Screen):
|
|||
try:
|
||||
self.diary_info.update(f"Diary: {self.diary_name}")
|
||||
except Exception as e:
|
||||
# If even this fails, at least try to show something
|
||||
self.diary_info.update(f"Diary: {self.diary_id}")
|
||||
|
||||
def refresh_entries(self):
|
||||
|
|
@ -120,14 +165,10 @@ class EditEntryScreen(Screen):
|
|||
service_manager = self.app.service_manager
|
||||
entry_service = service_manager.get_entry_service()
|
||||
|
||||
# Get all entries for this diary
|
||||
all_entries = entry_service.read_all()
|
||||
self.entries = [entry for entry in all_entries if entry.fk_travel_diary_id == self.diary_id]
|
||||
|
||||
# Sort by ID
|
||||
self.entries.sort(key=lambda x: x.id)
|
||||
|
||||
# Update next entry ID
|
||||
if self.entries:
|
||||
self.next_entry_id = max(entry.id for entry in self.entries) + 1
|
||||
else:
|
||||
|
|
@ -139,41 +180,8 @@ class EditEntryScreen(Screen):
|
|||
except Exception as e:
|
||||
self.notify(f"Error loading entries: {str(e)}")
|
||||
|
||||
# Ensure diary info is updated even if entries fail to load
|
||||
self._ensure_diary_info_updated()
|
||||
|
||||
async def async_refresh_entries(self):
|
||||
"""Asynchronous version of refresh"""
|
||||
if self.is_refreshing:
|
||||
return
|
||||
|
||||
self.is_refreshing = True
|
||||
|
||||
try:
|
||||
service_manager = self.app.service_manager
|
||||
entry_service = service_manager.get_entry_service()
|
||||
|
||||
# For now, use synchronous method since mock doesn't have async
|
||||
all_entries = entry_service.read_all()
|
||||
self.entries = [entry for entry in all_entries if entry.fk_travel_diary_id == self.diary_id]
|
||||
|
||||
# Sort by ID
|
||||
self.entries.sort(key=lambda x: x.id)
|
||||
|
||||
# Update next entry ID
|
||||
if self.entries:
|
||||
self.next_entry_id = max(entry.id for entry in self.entries) + 1
|
||||
else:
|
||||
self.next_entry_id = 1
|
||||
|
||||
self._update_entry_display()
|
||||
self._update_sub_header()
|
||||
|
||||
except Exception as e:
|
||||
self.notify(f"Error loading entries: {str(e)}")
|
||||
finally:
|
||||
self.is_refreshing = False
|
||||
|
||||
def _update_status_indicator(self, text: str, css_class: str):
|
||||
"""Helper to update status indicator text and class."""
|
||||
self.status_indicator.update(text)
|
||||
|
|
@ -211,6 +219,8 @@ class EditEntryScreen(Screen):
|
|||
"""Finishes the display update by reactivating change detection"""
|
||||
self._updating_display = False
|
||||
self._update_sub_header()
|
||||
if self.sidebar_visible:
|
||||
self._update_sidebar_content()
|
||||
|
||||
def _update_entry_display(self):
|
||||
"""Updates the display of the current entry"""
|
||||
|
|
@ -237,6 +247,342 @@ class EditEntryScreen(Screen):
|
|||
|
||||
self.call_after_refresh(self._finish_display_update)
|
||||
|
||||
def _update_sidebar_content(self):
|
||||
"""Updates the sidebar content with photos for the current diary"""
|
||||
photos = self._load_photos_for_diary(self.diary_id)
|
||||
|
||||
# Clear existing options safely
|
||||
self.photo_list.clear_options()
|
||||
|
||||
# Add 'Ingest Photo' option at the top
|
||||
self.photo_list.add_option("➕ Ingest Photo")
|
||||
|
||||
if not photos:
|
||||
self.photo_info.update("No photos found for this diary")
|
||||
self.help_text.update("📸 No photos available\n\nUse Photo Manager to add photos")
|
||||
return
|
||||
|
||||
# Add photos to the list
|
||||
for photo in photos:
|
||||
self.photo_list.add_option(f"📷 {photo.name}")
|
||||
|
||||
self.photo_info.update(f"📸 {len(photos)} photos in diary")
|
||||
|
||||
# English, visually distinct help text
|
||||
help_text = (
|
||||
"[b]⌨️ Sidebar Shortcuts[/b]\n"
|
||||
"[b][green]i[/green][/b]: Insert photo into entry\n"
|
||||
"[b][green]n[/green][/b]: Add new photo\n"
|
||||
"[b][green]d[/green][/b]: Delete selected photo\n"
|
||||
"[b][green]e[/green][/b]: Edit selected photo\n"
|
||||
"[b][yellow]Tab[/yellow][/b]: Back to editor\n"
|
||||
"[b][yellow]F8[/yellow][/b]: Show/hide sidebar\n"
|
||||
"[b][yellow]F9[/yellow][/b]: Switch focus (if needed)"
|
||||
)
|
||||
self.help_text.update(help_text)
|
||||
|
||||
def _load_photos_for_diary(self, diary_id: int) -> List[Photo]:
|
||||
"""Loads all photos for the specific diary"""
|
||||
try:
|
||||
service_manager = self.app.service_manager
|
||||
photo_service = service_manager.get_photo_service()
|
||||
|
||||
all_photos = photo_service.read_all()
|
||||
photos = [photo for photo in all_photos if photo.fk_travel_diary_id == diary_id]
|
||||
photos.sort(key=lambda x: x.id)
|
||||
return photos
|
||||
except Exception as e:
|
||||
self.notify(f"Error loading photos: {str(e)}")
|
||||
return []
|
||||
|
||||
def action_toggle_sidebar(self):
|
||||
"""Toggles the sidebar visibility"""
|
||||
print("DEBUG: TOGGLE SIDEBAR", self.sidebar_visible)
|
||||
self.sidebar_visible = not self.sidebar_visible
|
||||
|
||||
if self.sidebar_visible:
|
||||
self.sidebar.display = True
|
||||
self._update_sidebar_content()
|
||||
# Automatically focus the sidebar when opening
|
||||
self.sidebar_focused = True
|
||||
self.photo_list.focus()
|
||||
# Notification when opening the sidebar for the first time
|
||||
if not self._sidebar_opened_once:
|
||||
self.notify(
|
||||
"Sidebar opened and focused! Use the shortcuts shown in the help panel.",
|
||||
severity="info"
|
||||
)
|
||||
self._sidebar_opened_once = True
|
||||
else:
|
||||
self.sidebar.display = False
|
||||
self.sidebar_focused = False # Reset focus when hiding
|
||||
self.text_entry.focus() # Return focus to editor
|
||||
|
||||
# Update footer after context change
|
||||
self._update_footer_context()
|
||||
self.refresh(layout=True)
|
||||
|
||||
def action_toggle_focus(self):
|
||||
"""Toggles focus between editor and sidebar"""
|
||||
print("DEBUG: TOGGLE FOCUS", self.sidebar_visible, self.sidebar_focused)
|
||||
if not self.sidebar_visible:
|
||||
# If sidebar is not visible, show it and focus it
|
||||
self.action_toggle_sidebar()
|
||||
return
|
||||
|
||||
self.sidebar_focused = not self.sidebar_focused
|
||||
if self.sidebar_focused:
|
||||
self.photo_list.focus()
|
||||
else:
|
||||
self.text_entry.focus()
|
||||
|
||||
# Update footer after focus change
|
||||
self._update_footer_context()
|
||||
|
||||
def action_insert_photo(self):
|
||||
"""Insert selected photo into text"""
|
||||
if not self.sidebar_focused or not self.sidebar_visible:
|
||||
self.notify("Use F8 to open the sidebar first.", severity="warning")
|
||||
return
|
||||
|
||||
# Get selected photo
|
||||
if self.photo_list.highlighted is None:
|
||||
self.notify("No photo selected", severity="warning")
|
||||
return
|
||||
|
||||
# Adjust index because of 'Ingest Photo' at the top
|
||||
photo_index = self.photo_list.highlighted - 1
|
||||
|
||||
photos = self._load_photos_for_diary(self.diary_id)
|
||||
if photo_index < 0 or photo_index >= len(photos):
|
||||
self.notify("No photo selected", severity="warning")
|
||||
return
|
||||
|
||||
selected_photo = photos[photo_index]
|
||||
|
||||
# Insert photo reference into text
|
||||
photo_ref = f"\n[📷 {selected_photo.name}]({selected_photo.filepath})\n"
|
||||
if selected_photo.caption:
|
||||
photo_ref += f"*{selected_photo.caption}*\n"
|
||||
|
||||
# Insert at cursor position or at end
|
||||
current_text = self.text_entry.text
|
||||
cursor_position = len(current_text) # Insert at end for now
|
||||
new_text = current_text + photo_ref
|
||||
self.text_entry.text = new_text
|
||||
|
||||
self.notify(f"Inserted photo: {selected_photo.name}")
|
||||
|
||||
def action_ingest_new_photo(self):
|
||||
"""Ingest a new photo using modal"""
|
||||
if not self.sidebar_focused or not self.sidebar_visible:
|
||||
self.notify("Use F8 to open the sidebar first.", severity="warning")
|
||||
return
|
||||
|
||||
# Open add photo modal
|
||||
self.app.push_screen(
|
||||
AddPhotoModal(diary_id=self.diary_id),
|
||||
self.handle_add_photo_result
|
||||
)
|
||||
|
||||
def handle_add_photo_result(self, result: dict | None) -> None:
|
||||
"""Callback that processes the add photo modal result."""
|
||||
if result is None:
|
||||
self.notify("Add photo cancelled")
|
||||
return
|
||||
|
||||
# Photo was already created in the modal, just refresh the sidebar
|
||||
if self.sidebar_visible:
|
||||
self._update_sidebar_content()
|
||||
self.notify(f"Photo '{result['name']}' added successfully!")
|
||||
|
||||
async def _async_create_photo(self, photo_data: dict):
|
||||
"""Creates a new photo asynchronously"""
|
||||
try:
|
||||
service_manager = self.app.service_manager
|
||||
photo_service = service_manager.get_photo_service()
|
||||
|
||||
current_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
new_photo = photo_service.create(
|
||||
filepath=Path(photo_data["filepath"]),
|
||||
name=photo_data["name"],
|
||||
travel_diary_id=self.diary_id,
|
||||
addition_date=current_date,
|
||||
caption=photo_data["caption"]
|
||||
)
|
||||
|
||||
if new_photo:
|
||||
self.notify(f"Photo '{new_photo.name}' added successfully!")
|
||||
# Refresh sidebar content
|
||||
if self.sidebar_visible:
|
||||
self._update_sidebar_content()
|
||||
else:
|
||||
self.notify("Error creating photo")
|
||||
|
||||
except Exception as e:
|
||||
self.notify(f"Error creating photo: {str(e)}")
|
||||
|
||||
def action_delete_photo(self):
|
||||
"""Delete selected photo"""
|
||||
if not self.sidebar_focused or not self.sidebar_visible:
|
||||
self.notify("Use F8 to open the sidebar first.", severity="warning")
|
||||
return
|
||||
|
||||
if self.photo_list.highlighted is None:
|
||||
self.notify("No photo selected", severity="warning")
|
||||
return
|
||||
|
||||
# Adjust index because of 'Ingest Photo' at the top
|
||||
photo_index = self.photo_list.highlighted - 1
|
||||
|
||||
photos = self._load_photos_for_diary(self.diary_id)
|
||||
if photo_index < 0 or photo_index >= len(photos):
|
||||
self.notify("No photo selected", severity="warning")
|
||||
return
|
||||
|
||||
selected_photo = photos[photo_index]
|
||||
|
||||
# Open confirm delete modal
|
||||
self.app.push_screen(
|
||||
ConfirmDeleteModal(photo=selected_photo),
|
||||
self.handle_delete_photo_result
|
||||
)
|
||||
|
||||
def handle_delete_photo_result(self, result: bool) -> None:
|
||||
"""Callback that processes the delete photo modal result."""
|
||||
if result:
|
||||
# Get the selected photo with adjusted index
|
||||
photos = self._load_photos_for_diary(self.diary_id)
|
||||
photo_index = self.photo_list.highlighted - 1 # Adjust for 'Ingest Photo' at top
|
||||
|
||||
if self.photo_list.highlighted is None or photo_index < 0 or photo_index >= len(photos):
|
||||
self.notify("Photo no longer available", severity="error")
|
||||
return
|
||||
|
||||
selected_photo = photos[photo_index]
|
||||
|
||||
# Schedule async deletion
|
||||
self.call_later(self._async_delete_photo, selected_photo)
|
||||
else:
|
||||
self.notify("Delete cancelled")
|
||||
|
||||
async def _async_delete_photo(self, photo: Photo):
|
||||
"""Deletes a photo asynchronously"""
|
||||
try:
|
||||
service_manager = self.app.service_manager
|
||||
photo_service = service_manager.get_photo_service()
|
||||
|
||||
result = photo_service.delete(photo)
|
||||
|
||||
if result:
|
||||
self.notify(f"Photo '{photo.name}' deleted successfully!")
|
||||
# Refresh sidebar content
|
||||
if self.sidebar_visible:
|
||||
self._update_sidebar_content()
|
||||
else:
|
||||
self.notify("Error deleting photo")
|
||||
|
||||
except Exception as e:
|
||||
self.notify(f"Error deleting photo: {str(e)}")
|
||||
|
||||
def action_edit_photo(self):
|
||||
"""Edit selected photo using modal"""
|
||||
if not self.sidebar_focused or not self.sidebar_visible:
|
||||
self.notify("Use F8 to open the sidebar first.", severity="warning")
|
||||
return
|
||||
|
||||
if self.photo_list.highlighted is None:
|
||||
self.notify("No photo selected", severity="warning")
|
||||
return
|
||||
|
||||
# Adjust index because of 'Ingest Photo' at the top
|
||||
photo_index = self.photo_list.highlighted - 1
|
||||
|
||||
photos = self._load_photos_for_diary(self.diary_id)
|
||||
if photo_index < 0 or photo_index >= len(photos):
|
||||
self.notify("No photo selected", severity="warning")
|
||||
return
|
||||
|
||||
selected_photo = photos[photo_index]
|
||||
|
||||
# Open edit photo modal
|
||||
self.app.push_screen(
|
||||
EditPhotoModal(photo=selected_photo),
|
||||
self.handle_edit_photo_result
|
||||
)
|
||||
|
||||
def handle_edit_photo_result(self, result: dict | None) -> None:
|
||||
"""Callback that processes the edit photo modal result."""
|
||||
if result is None:
|
||||
self.notify("Edit photo cancelled")
|
||||
return
|
||||
|
||||
# Get the selected photo with adjusted index
|
||||
photos = self._load_photos_for_diary(self.diary_id)
|
||||
photo_index = self.photo_list.highlighted - 1 # Adjust for 'Ingest Photo' at top
|
||||
|
||||
if self.photo_list.highlighted is None or photo_index < 0 or photo_index >= len(photos):
|
||||
self.notify("Photo no longer available", severity="error")
|
||||
return
|
||||
|
||||
selected_photo = photos[photo_index]
|
||||
|
||||
# Schedule async update
|
||||
self.call_later(self._async_update_photo, selected_photo, result)
|
||||
|
||||
async def _async_update_photo(self, original_photo: Photo, photo_data: dict):
|
||||
"""Updates a photo asynchronously"""
|
||||
try:
|
||||
service_manager = self.app.service_manager
|
||||
photo_service = service_manager.get_photo_service()
|
||||
|
||||
# Create updated photo object
|
||||
updated_photo = Photo(
|
||||
filepath=photo_data["filepath"],
|
||||
name=photo_data["name"],
|
||||
addition_date=original_photo.addition_date,
|
||||
caption=photo_data["caption"],
|
||||
entries=original_photo.entries if original_photo.entries is not None else [],
|
||||
id=original_photo.id
|
||||
)
|
||||
|
||||
result = photo_service.update(original_photo, updated_photo)
|
||||
|
||||
if result:
|
||||
self.notify(f"Photo '{updated_photo.name}' updated successfully!")
|
||||
# Refresh sidebar content
|
||||
if self.sidebar_visible:
|
||||
self._update_sidebar_content()
|
||||
else:
|
||||
self.notify("Error updating photo")
|
||||
|
||||
except Exception as e:
|
||||
self.notify(f"Error updating photo: {str(e)}")
|
||||
|
||||
def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None:
|
||||
"""Handles photo selection in the sidebar"""
|
||||
if not self.sidebar_visible:
|
||||
return
|
||||
# If 'Ingest Photo' is selected (always index 0)
|
||||
if event.option_index == 0:
|
||||
self.action_ingest_new_photo()
|
||||
return
|
||||
photos = self._load_photos_for_diary(self.diary_id)
|
||||
# Adjust index because of 'Ingest Photo' at the top
|
||||
photo_index = event.option_index - 1
|
||||
if not photos or photo_index >= len(photos):
|
||||
return
|
||||
selected_photo = photos[photo_index]
|
||||
self.notify(f"Selected photo: {selected_photo.name}")
|
||||
# Update photo info with details
|
||||
photo_details = f"📷 {selected_photo.name}\n"
|
||||
photo_details += f"📅 {selected_photo.addition_date}\n"
|
||||
if selected_photo.caption:
|
||||
photo_details += f"💬 {selected_photo.caption}\n"
|
||||
photo_details += f"📁 {selected_photo.filepath}"
|
||||
self.photo_info.update(photo_details)
|
||||
|
||||
def on_text_area_changed(self, event) -> None:
|
||||
"""Detects text changes to mark as unsaved"""
|
||||
if (hasattr(self, 'text_entry') and not self.text_entry.read_only and
|
||||
|
|
@ -251,6 +597,17 @@ class EditEntryScreen(Screen):
|
|||
self.has_unsaved_changes = False
|
||||
self._update_sub_header()
|
||||
|
||||
def on_focus(self, event) -> None:
|
||||
"""Captures focus changes to update footer"""
|
||||
# Check if focus changed to/from sidebar
|
||||
if hasattr(event.widget, 'id'):
|
||||
if event.widget.id == "photo_list":
|
||||
self.sidebar_focused = True
|
||||
self._update_footer_context()
|
||||
elif event.widget.id == "text_entry":
|
||||
self.sidebar_focused = False
|
||||
self._update_footer_context()
|
||||
|
||||
def action_back_to_list(self) -> None:
|
||||
"""Goes back to the diary list"""
|
||||
if self.is_new_entry and not self.text_entry.text.strip() and not self.has_unsaved_changes:
|
||||
|
|
@ -375,7 +732,6 @@ class EditEntryScreen(Screen):
|
|||
service_manager = self.app.service_manager
|
||||
entry_service = service_manager.get_entry_service()
|
||||
|
||||
# Get current date as datetime object
|
||||
current_date = datetime.now()
|
||||
|
||||
new_entry = entry_service.create(
|
||||
|
|
@ -389,7 +745,6 @@ class EditEntryScreen(Screen):
|
|||
self.entries.append(new_entry)
|
||||
self.entries.sort(key=lambda x: x.id)
|
||||
|
||||
# Find the new entry index
|
||||
for i, entry in enumerate(self.entries):
|
||||
if entry.id == new_entry.id:
|
||||
self.current_entry_index = i
|
||||
|
|
@ -419,7 +774,6 @@ class EditEntryScreen(Screen):
|
|||
current_entry = self.entries[self.current_entry_index]
|
||||
updated_content = self.text_entry.text
|
||||
|
||||
# Create updated entry object
|
||||
updated_entry = Entry(
|
||||
title=current_entry.title,
|
||||
text=updated_content,
|
||||
|
|
@ -445,8 +799,44 @@ class EditEntryScreen(Screen):
|
|||
except Exception as e:
|
||||
self.notify(f"Error updating entry: {str(e)}")
|
||||
|
||||
def action_force_refresh(self):
|
||||
"""Forces manual refresh"""
|
||||
self.notify("Forcing refresh...")
|
||||
self.refresh_entries()
|
||||
self.call_later(self.async_refresh_entries)
|
||||
def on_key(self, event):
|
||||
# Sidebar contextual shortcuts
|
||||
if self.sidebar_focused and self.sidebar_visible:
|
||||
if event.key == "i":
|
||||
self.action_insert_photo()
|
||||
event.stop()
|
||||
elif event.key == "n":
|
||||
self.action_ingest_new_photo()
|
||||
event.stop()
|
||||
elif event.key == "d":
|
||||
self.action_delete_photo()
|
||||
event.stop()
|
||||
elif event.key == "e":
|
||||
self.action_edit_photo()
|
||||
event.stop()
|
||||
# Shift+Tab: remove indent
|
||||
elif self.focused is self.text_entry and event.key == "shift+tab":
|
||||
textarea = self.text_entry
|
||||
row, col = textarea.cursor_location
|
||||
lines = textarea.text.splitlines()
|
||||
if row < len(lines):
|
||||
line = lines[row]
|
||||
if line.startswith('\t'):
|
||||
lines[row] = line[1:]
|
||||
textarea.text = '\n'.join(lines)
|
||||
textarea.cursor_location = (row, max(col - 1, 0))
|
||||
elif line.startswith(' '): # 4 spaces
|
||||
lines[row] = line[4:]
|
||||
textarea.text = '\n'.join(lines)
|
||||
textarea.cursor_location = (row, max(col - 4, 0))
|
||||
elif line.startswith(' '):
|
||||
n = len(line) - len(line.lstrip(' '))
|
||||
to_remove = min(n, 4)
|
||||
lines[row] = line[to_remove:]
|
||||
textarea.text = '\n'.join(lines)
|
||||
textarea.cursor_location = (row, max(col - to_remove, 0))
|
||||
event.stop()
|
||||
# Tab: insert tab
|
||||
elif self.focused is self.text_entry and event.key == "tab":
|
||||
self.text_entry.insert('\t')
|
||||
event.stop()
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
import os
|
||||
from pathlib import Path
|
||||
from textual.app import ComposeResult
|
||||
from textual.screen import Screen
|
||||
from textual.widgets import Static, Input, Button
|
||||
from textual.containers import Horizontal, Container
|
||||
from .file_picker_modal import FilePickerModal
|
||||
|
||||
class AddPhotoModal(Screen):
|
||||
"""Modal for adding a new photo"""
|
||||
def __init__(self, diary_id: int):
|
||||
super().__init__()
|
||||
self.diary_id = diary_id
|
||||
self.result = None
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Container(
|
||||
Static("📷 Add New Photo", classes="AddPhotoModal-Title"),
|
||||
Static("File path:", classes="AddPhotoModal-Label"),
|
||||
Horizontal(
|
||||
Input(placeholder="Enter file path...", id="filepath-input", classes="AddPhotoModal-Input"),
|
||||
Button("Escolher arquivo...", id="choose-file-button", classes="AddPhotoModal-Button"),
|
||||
classes="AddPhotoModal-FileRow"
|
||||
),
|
||||
Static("Photo name:", classes="AddPhotoModal-Label"),
|
||||
Input(placeholder="Enter photo name...", id="name-input", classes="AddPhotoModal-Input"),
|
||||
Static("Caption (optional):", classes="AddPhotoModal-Label"),
|
||||
Input(placeholder="Enter caption...", id="caption-input", classes="AddPhotoModal-Input"),
|
||||
Horizontal(
|
||||
Button("Add Photo", id="add-button", classes="AddPhotoModal-Button"),
|
||||
Button("Cancel", id="cancel-button", classes="AddPhotoModal-Button"),
|
||||
classes="AddPhotoModal-Buttons"
|
||||
),
|
||||
classes="AddPhotoModal-Dialog"
|
||||
)
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
if event.button.id == "choose-file-button":
|
||||
self.app.push_screen(
|
||||
FilePickerModal(),
|
||||
self.handle_file_picker_result
|
||||
)
|
||||
return
|
||||
if event.button.id == "add-button":
|
||||
filepath = self.query_one("#filepath-input", Input).value
|
||||
name = self.query_one("#name-input", Input).value
|
||||
caption = self.query_one("#caption-input", Input).value
|
||||
if not filepath.strip() or not name.strip():
|
||||
self.notify("File path and name are required", severity="error")
|
||||
return
|
||||
|
||||
# Try to create the photo in the database
|
||||
self.call_later(self._async_create_photo, {
|
||||
"filepath": filepath.strip(),
|
||||
"name": name.strip(),
|
||||
"caption": caption.strip() if caption.strip() else None
|
||||
})
|
||||
elif event.button.id == "cancel-button":
|
||||
self.dismiss()
|
||||
|
||||
async def _async_create_photo(self, photo_data: dict):
|
||||
"""Creates a new photo asynchronously using PhotoService"""
|
||||
try:
|
||||
service_manager = self.app.service_manager
|
||||
photo_service = service_manager.get_photo_service()
|
||||
|
||||
new_photo = photo_service.create(
|
||||
filepath=Path(photo_data["filepath"]),
|
||||
name=photo_data["name"],
|
||||
travel_diary_id=self.diary_id,
|
||||
caption=photo_data["caption"]
|
||||
)
|
||||
|
||||
if new_photo:
|
||||
self.notify(f"Photo '{new_photo.name}' added successfully!")
|
||||
# Return the created photo data to the calling screen
|
||||
self.result = {
|
||||
"filepath": photo_data["filepath"],
|
||||
"name": photo_data["name"],
|
||||
"caption": photo_data["caption"],
|
||||
"photo_id": new_photo.id
|
||||
}
|
||||
self.dismiss(self.result)
|
||||
else:
|
||||
self.notify("Error creating photo in database", severity="error")
|
||||
|
||||
except Exception as e:
|
||||
self.notify(f"Error creating photo: {str(e)}", severity="error")
|
||||
|
||||
def handle_file_picker_result(self, result: str | None) -> None:
|
||||
if result:
|
||||
# Set the filepath input value
|
||||
filepath_input = self.query_one("#filepath-input", Input)
|
||||
filepath_input.value = result
|
||||
# Trigger the input change event to update the UI
|
||||
filepath_input.refresh()
|
||||
# Auto-fill the name field with the filename (without extension)
|
||||
filename = Path(result).stem
|
||||
name_input = self.query_one("#name-input", Input)
|
||||
if not name_input.value.strip():
|
||||
name_input.value = filename
|
||||
name_input.refresh()
|
||||
else:
|
||||
# User cancelled the file picker
|
||||
self.notify("File selection cancelled", severity="information")
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
from textual.app import ComposeResult
|
||||
from textual.screen import Screen
|
||||
from textual.widgets import Static, Button
|
||||
from textual.containers import Container, Horizontal
|
||||
from pilgrim.models.photo import Photo
|
||||
|
||||
class ConfirmDeleteModal(Screen):
|
||||
"""Modal for confirming photo deletion"""
|
||||
def __init__(self, photo: Photo):
|
||||
super().__init__()
|
||||
self.photo = photo
|
||||
self.result = None
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Container(
|
||||
Static("🗑️ Confirm Deletion", classes="ConfirmDeleteModal-Title"),
|
||||
Static(f"Are you sure you want to delete the photo '{self.photo.name}'?", classes="ConfirmDeleteModal-Message"),
|
||||
Static("This action cannot be undone.", classes="ConfirmDeleteModal-Warning"),
|
||||
Horizontal(
|
||||
Button("Delete", variant="error", id="delete-button", classes="ConfirmDeleteModal-Button"),
|
||||
Button("Cancel", variant="default", id="cancel-button", classes="ConfirmDeleteModal-Button"),
|
||||
classes="ConfirmDeleteModal-Buttons"
|
||||
),
|
||||
classes="ConfirmDeleteModal-Dialog"
|
||||
)
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
if event.button.id == "delete-button":
|
||||
self.result = True
|
||||
self.dismiss(True)
|
||||
elif event.button.id == "cancel-button":
|
||||
self.dismiss(False)
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
from textual.app import ComposeResult
|
||||
from textual.screen import Screen
|
||||
from textual.widgets import Static, Input, Button
|
||||
from textual.containers import Container, Horizontal
|
||||
from pilgrim.models.photo import Photo
|
||||
|
||||
class EditPhotoModal(Screen):
|
||||
"""Modal for editing an existing photo (name and caption only)"""
|
||||
def __init__(self, photo: Photo):
|
||||
super().__init__()
|
||||
self.photo = photo
|
||||
self.result = None
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Container(
|
||||
Static("✏️ Edit Photo", classes="EditPhotoModal-Title"),
|
||||
Static("File path (read-only):", classes="EditPhotoModal-Label"),
|
||||
Input(
|
||||
value=self.photo.filepath,
|
||||
id="filepath-input",
|
||||
classes="EditPhotoModal-Input",
|
||||
disabled=True
|
||||
),
|
||||
Static("Photo name:", classes="EditPhotoModal-Label"),
|
||||
Input(
|
||||
value=self.photo.name,
|
||||
placeholder="Enter photo name...",
|
||||
id="name-input",
|
||||
classes="EditPhotoModal-Input"
|
||||
),
|
||||
Static("Caption (optional):", classes="EditPhotoModal-Label"),
|
||||
Input(
|
||||
value=self.photo.caption or "",
|
||||
placeholder="Enter caption...",
|
||||
id="caption-input",
|
||||
classes="EditPhotoModal-Input"
|
||||
),
|
||||
Horizontal(
|
||||
Button("Save Changes", id="save-button", classes="EditPhotoModal-Button"),
|
||||
Button("Cancel", id="cancel-button", classes="EditPhotoModal-Button"),
|
||||
classes="EditPhotoModal-Buttons"
|
||||
),
|
||||
classes="EditPhotoModal-Dialog"
|
||||
)
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
if event.button.id == "save-button":
|
||||
name = self.query_one("#name-input", Input).value
|
||||
caption = self.query_one("#caption-input", Input).value
|
||||
|
||||
if not name.strip():
|
||||
self.notify("Photo name is required", severity="error")
|
||||
return
|
||||
|
||||
# Return the updated photo data
|
||||
self.result = {
|
||||
"filepath": self.photo.filepath, # Keep original filepath
|
||||
"name": name.strip(),
|
||||
"caption": caption.strip() if caption.strip() else None
|
||||
}
|
||||
self.dismiss(self.result)
|
||||
|
||||
elif event.button.id == "cancel-button":
|
||||
self.dismiss()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
"""Focus on the name input when modal opens"""
|
||||
self.query_one("#name-input", Input).focus()
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
import os
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
from textual.app import ComposeResult
|
||||
from textual.screen import Screen
|
||||
from textual.widgets import Static, DirectoryTree, Button
|
||||
from textual.containers import Horizontal, Container
|
||||
|
||||
class ImageDirectoryTree(DirectoryTree):
|
||||
"""DirectoryTree that only shows image files"""
|
||||
|
||||
def filter_paths(self, paths: Iterable[Path]) -> Iterable[Path]:
|
||||
"""Filter to show only directories and image files"""
|
||||
image_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'}
|
||||
return [
|
||||
path for path in paths
|
||||
if path.is_dir() or path.suffix.lower() in image_extensions
|
||||
]
|
||||
|
||||
class FilePickerModal(Screen):
|
||||
"""Modal for picking an image file using DirectoryTree"""
|
||||
|
||||
def __init__(self, start_path=None):
|
||||
super().__init__()
|
||||
self.start_path = Path(start_path or os.getcwd())
|
||||
# Start one level up to make navigation easier
|
||||
self.current_path = self.start_path.parent
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Container(
|
||||
Static(f"Current: {self.current_path}", id="title", classes="FilePickerModal-Title"),
|
||||
ImageDirectoryTree(str(self.current_path), id="directory-tree"),
|
||||
Horizontal(
|
||||
Button("Up", id="up-button", classes="FilePickerModal-Button"),
|
||||
Button("Cancel", id="cancel-button", classes="FilePickerModal-Button"),
|
||||
classes="FilePickerModal-Buttons"
|
||||
),
|
||||
classes="FilePickerModal-Dialog"
|
||||
)
|
||||
|
||||
def on_directory_tree_file_selected(self, event: DirectoryTree.FileSelected) -> None:
|
||||
"""Handle file selection"""
|
||||
file_path = event.path
|
||||
# Check if it's an image file
|
||||
image_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'}
|
||||
if file_path.suffix.lower() in image_extensions:
|
||||
# Return the file path as result
|
||||
self.dismiss(str(file_path))
|
||||
else:
|
||||
self.notify("Please select an image file", severity="warning")
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
"""Handle button presses"""
|
||||
if event.button.id == "up-button":
|
||||
# Navigate to parent directory
|
||||
parent = self.current_path.parent
|
||||
if parent != self.current_path:
|
||||
self.current_path = parent
|
||||
self.query_one("#title", Static).update(f"Current: {self.current_path}")
|
||||
# Reload the directory tree
|
||||
tree = self.query_one("#directory-tree", ImageDirectoryTree)
|
||||
tree.path = str(self.current_path)
|
||||
tree.reload()
|
||||
elif event.button.id == "cancel-button":
|
||||
# Return None to indicate cancellation
|
||||
self.dismiss(None)
|
||||
|
|
@ -387,4 +387,243 @@ Screen.-modal {
|
|||
.RenameEntryModal-cancel-button {
|
||||
margin: 0 1;
|
||||
width: 1fr;
|
||||
}
|
||||
|
||||
.EditEntryScreen-sidebar {
|
||||
width: 40;
|
||||
min-height: 10;
|
||||
border-left: solid green;
|
||||
padding: 1;
|
||||
background: $surface-darken-2;
|
||||
color: $primary;
|
||||
text-style: bold;
|
||||
content-align: left top;
|
||||
}
|
||||
|
||||
.EditEntryScreen-content-container {
|
||||
layout: horizontal;
|
||||
height: 1fr;
|
||||
}
|
||||
|
||||
.EditEntryScreen-sidebar-title {
|
||||
text-align: center;
|
||||
text-style: bold;
|
||||
color: $accent;
|
||||
padding: 1;
|
||||
border-bottom: solid $accent;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
.EditEntryScreen-sidebar-content {
|
||||
height: 1fr;
|
||||
layout: vertical;
|
||||
}
|
||||
|
||||
.EditEntryScreen-sidebar-photo-list {
|
||||
height: 1fr;
|
||||
border: solid $accent;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
.EditEntryScreen-sidebar-photo-info {
|
||||
height: auto;
|
||||
min-height: 3;
|
||||
border: solid $warning;
|
||||
padding: 1;
|
||||
margin-bottom: 1;
|
||||
background: $surface-darken-1;
|
||||
}
|
||||
|
||||
.EditEntryScreen-sidebar-help {
|
||||
height: auto;
|
||||
min-height: 8;
|
||||
border: solid $success;
|
||||
padding: 1;
|
||||
background: $surface-darken-1;
|
||||
text-style: italic;
|
||||
}
|
||||
|
||||
/* Photo Modal Styles */
|
||||
.modal-dialog {
|
||||
layout: vertical;
|
||||
width: 60%;
|
||||
height: auto;
|
||||
background: $surface;
|
||||
border: thick $accent;
|
||||
padding: 2 4;
|
||||
align: center middle;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
text-align: center;
|
||||
text-style: bold;
|
||||
color: $primary;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
.modal-label {
|
||||
margin-bottom: 1;
|
||||
color: $text;
|
||||
}
|
||||
|
||||
.modal-input {
|
||||
width: 1fr;
|
||||
margin-bottom: 2;
|
||||
}
|
||||
|
||||
.modal-buttons {
|
||||
width: 1fr;
|
||||
height: auto;
|
||||
align: center middle;
|
||||
padding-top: 1;
|
||||
}
|
||||
|
||||
.modal-button {
|
||||
margin: 0 1;
|
||||
width: 1fr;
|
||||
}
|
||||
|
||||
/* AddPhotoModal styles */
|
||||
.AddPhotoModal-Dialog {
|
||||
layout: vertical;
|
||||
width: 60%;
|
||||
height: auto;
|
||||
background: $surface;
|
||||
border: thick $accent;
|
||||
padding: 2 4;
|
||||
align: center middle;
|
||||
}
|
||||
.AddPhotoModal-Title {
|
||||
text-align: center;
|
||||
text-style: bold;
|
||||
color: $primary;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
.AddPhotoModal-Label {
|
||||
margin-bottom: 1;
|
||||
color: $text;
|
||||
}
|
||||
.AddPhotoModal-Input {
|
||||
width: 1fr;
|
||||
margin-bottom: 2;
|
||||
}
|
||||
.AddPhotoModal-Buttons {
|
||||
width: 1fr;
|
||||
height: auto;
|
||||
align: center middle;
|
||||
padding-top: 1;
|
||||
}
|
||||
.AddPhotoModal-Button {
|
||||
margin: 0 1;
|
||||
width: 1fr;
|
||||
}
|
||||
|
||||
/* EditPhotoModal styles */
|
||||
.EditPhotoModal-Dialog {
|
||||
layout: vertical;
|
||||
width: 60%;
|
||||
height: auto;
|
||||
background: $surface;
|
||||
border: thick $accent;
|
||||
padding: 2 4;
|
||||
align: center middle;
|
||||
}
|
||||
.EditPhotoModal-Title {
|
||||
text-align: center;
|
||||
text-style: bold;
|
||||
color: $primary;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
.EditPhotoModal-Label {
|
||||
margin-bottom: 1;
|
||||
color: $text;
|
||||
}
|
||||
.EditPhotoModal-Input {
|
||||
width: 1fr;
|
||||
margin-bottom: 2;
|
||||
}
|
||||
.EditPhotoModal-Buttons {
|
||||
width: 1fr;
|
||||
height: auto;
|
||||
align: center middle;
|
||||
padding-top: 1;
|
||||
}
|
||||
.EditPhotoModal-Button {
|
||||
margin: 0 1;
|
||||
width: 1fr;
|
||||
}
|
||||
|
||||
/* FilePickerModal styles */
|
||||
.FilePickerModal-Dialog {
|
||||
layout: vertical;
|
||||
width: 80%;
|
||||
height: 80%;
|
||||
background: $surface;
|
||||
border: thick $accent;
|
||||
padding: 2 4;
|
||||
align: center middle;
|
||||
}
|
||||
|
||||
.FilePickerModal-Title {
|
||||
text-align: center;
|
||||
text-style: bold;
|
||||
color: $primary;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
.FilePickerModal-Buttons {
|
||||
width: 1fr;
|
||||
height: auto;
|
||||
align: center middle;
|
||||
padding-top: 1;
|
||||
}
|
||||
|
||||
.FilePickerModal-Button {
|
||||
margin: 0 1;
|
||||
width: 1fr;
|
||||
}
|
||||
|
||||
/* DirectoryTree specific styles */
|
||||
#directory-tree {
|
||||
height: 1fr;
|
||||
border: solid $accent;
|
||||
margin: 1;
|
||||
}
|
||||
|
||||
/* ConfirmDeleteModal styles */
|
||||
.ConfirmDeleteModal-Dialog {
|
||||
layout: vertical;
|
||||
width: 60%;
|
||||
height: auto;
|
||||
background: $surface;
|
||||
border: thick $error;
|
||||
padding: 2 4;
|
||||
align: center middle;
|
||||
}
|
||||
.ConfirmDeleteModal-Title {
|
||||
text-align: center;
|
||||
text-style: bold;
|
||||
color: $error;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
.ConfirmDeleteModal-Message {
|
||||
text-align: center;
|
||||
color: $text;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
.ConfirmDeleteModal-Warning {
|
||||
text-align: center;
|
||||
color: $warning;
|
||||
text-style: italic;
|
||||
margin-bottom: 2;
|
||||
}
|
||||
.ConfirmDeleteModal-Buttons {
|
||||
width: 1fr;
|
||||
height: auto;
|
||||
align: center middle;
|
||||
padding-top: 1;
|
||||
}
|
||||
.ConfirmDeleteModal-Button {
|
||||
margin: 0 1;
|
||||
width: 1fr;
|
||||
}
|
||||
Loading…
Reference in New Issue