mirror of
https://github.com/gmbrax/Pilgrim.git
synced 2026-11-10 09:30:17 +00:00
Compare commits
45
Commits
@@ -0,0 +1,51 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Planned
|
||||
|
||||
- Installation Method 1 (repository compilation)
|
||||
- Organization of trips by date, location, or theme
|
||||
- Enhanced photo management features
|
||||
- Search functionality
|
||||
- Export features
|
||||
- Testing implementation
|
||||
|
||||
## [0.0.3] - 2025-07-07
|
||||
|
||||
### Changed
|
||||
- Removed the dependency on textual-dev from pyproject.toml
|
||||
|
||||
## [0.0.2] - 2025-07-07
|
||||
|
||||
### Changed
|
||||
- Changed the license in pyproject.toml to BSD
|
||||
|
||||
## [0.0.1] - 2025-07-06
|
||||
|
||||
### Added
|
||||
|
||||
- Initial alpha release of Pilgrim travel diary application
|
||||
- Create and edit travel diaries
|
||||
- Create and edit diary entries
|
||||
- Photo ingestion system
|
||||
- Photo addition and reference via sidebar
|
||||
- Text User Interface (TUI) built with Textual framework
|
||||
- Pre-compiled binary installation method (Method 2)
|
||||
- Support for Linux operating systems
|
||||
- Basic project documentation (README)
|
||||
|
||||
### Known Issues
|
||||
|
||||
- Installation Method 1 not yet implemented
|
||||
- No testing suite implemented yet
|
||||
- Some features may be unstable in an alpha version
|
||||
|
||||
[Unreleased]: https://github.com/username/pilgrim/compare/v0.0.1...HEAD
|
||||
|
||||
[0.0.1]: https://github.com/username/pilgrim/releases/tag/v0.0.1
|
||||
@@ -1,16 +1,17 @@
|
||||
# Python_Pilgrim
|
||||
# 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.
|
||||
**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
|
||||
- Store photos and add them to the diary entry
|
||||
|
||||
## Requirements
|
||||
- Python 3.8 or higher
|
||||
@@ -21,30 +22,25 @@
|
||||
|
||||
## 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
|
||||
```
|
||||
To install the application you must issue the command:
|
||||
|
||||
```bash
|
||||
pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple Pilgrim
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
To run the main application, execute:
|
||||
|
||||
```bash
|
||||
python <???>.py
|
||||
To use application you just issue:
|
||||
```bash
|
||||
pilgrim
|
||||
```
|
||||
|
||||
This will start the Python_Pilgrim application. Follow the on-screen instructions to create and manage your travel diaries.
|
||||
This will start the Pilgrim application. Follow the on-screen instructions to create and manage your travel diaries.
|
||||
|
||||
## Changelog
|
||||
|
||||
To see all the changes in the current version, please refer to the [CHANGELOG](CHANGELOG.md)
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the BSD 3-Clause License — see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
|
||||
[project]
|
||||
name = "Pilgrim"
|
||||
version = "0.0.1"
|
||||
version = "0.0.3"
|
||||
authors = [
|
||||
{ name="Gustavo Henrique Santos Souza de Miranda", email="gustavohssmiranda@gmail.com" }
|
||||
]
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
greenlet==3.2.3
|
||||
SQLAlchemy==2.0.41
|
||||
typing_extensions==4.14.0
|
||||
typing_extensions==4.14.1
|
||||
|
||||
|
||||
textual~=3.3.0
|
||||
textual~=4.0.0
|
||||
@@ -203,35 +203,36 @@ class DiaryListScreen(Screen):
|
||||
self.action_open_diary()
|
||||
|
||||
def action_new_diary(self):
|
||||
"""Action to create new diary"""
|
||||
"""
|
||||
Opens the modal dialog to create a new diary and registers a callback for submission.
|
||||
"""
|
||||
self.app.push_screen(NewDiaryModal(),self._on_new_diary_submitted)
|
||||
|
||||
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)
|
||||
def _on_new_diary_submitted(self, result):
|
||||
"""
|
||||
Handles the callback after a new diary is submitted.
|
||||
|
||||
If a diary was created (non-empty result), notifies the user and refreshes the diary list; otherwise, notifies that creation was canceled.
|
||||
"""
|
||||
if result: # Se result não é string vazia, o diário foi criado
|
||||
self.notify(f"Returning to diary list...")
|
||||
# Atualiza a lista de diários
|
||||
self.refresh_diaries()
|
||||
else:
|
||||
self.notify(f"Canceled...")
|
||||
|
||||
async def _async_create_diary(self,name: str):
|
||||
|
||||
try:
|
||||
service = self.app.service_manager.get_travel_diary_service()
|
||||
created_diary = await service.async_create(name)
|
||||
if created_diary:
|
||||
self.diary_id_map[created_diary.id] = created_diary.id
|
||||
await self.async_refresh_diaries()
|
||||
self.notify(f"Diary: '{name}' created!")
|
||||
else:
|
||||
self.notify("Error Creating the diary")
|
||||
except Exception as e:
|
||||
self.notify(f"Exception on creating the diary: {str(e)}")
|
||||
|
||||
self.notify(f"Creation canceled...")
|
||||
|
||||
def _on_screen_resume(self) -> None:
|
||||
"""
|
||||
Refreshes the diary list when the screen resumes.
|
||||
"""
|
||||
self.refresh_diaries()
|
||||
|
||||
def action_edit_selected_diary(self):
|
||||
"""Action to edit selected diary"""
|
||||
"""
|
||||
Opens the edit dialog for the currently selected diary.
|
||||
|
||||
If a diary is selected, displays the edit modal and registers a callback for when the edit is submitted. Notifies the user if no diary is selected.
|
||||
"""
|
||||
if self.selected_diary_index is not None:
|
||||
diary_id = self.diary_id_map.get(self.selected_diary_index)
|
||||
if diary_id:
|
||||
|
||||
@@ -8,9 +8,13 @@ from textual.widgets import Label, Input, Button
|
||||
class EditDiaryModal(ModalScreen[tuple[int,str]]):
|
||||
BINDINGS = [
|
||||
Binding("escape", "cancel", "Cancel"),
|
||||
Binding("enter", "edit_diary", "Save",priority=True),
|
||||
]
|
||||
|
||||
def __init__(self, diary_id: int):
|
||||
"""
|
||||
Initialize the EditDiaryModal with the specified diary ID, pre-filling the input field with the current diary name.
|
||||
"""
|
||||
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
|
||||
@@ -31,18 +35,48 @@ class EditDiaryModal(ModalScreen[tuple[int,str]]):
|
||||
self.name_input.cursor_position = len(self.name_input.value)
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
"""
|
||||
Handle button press events in the modal.
|
||||
|
||||
Triggers the save action if the "Save" button is pressed, or cancels and dismisses the modal if the "Cancel" button is pressed.
|
||||
"""
|
||||
if event.button.id == "save_diary_button":
|
||||
new_diary_name = self.name_input.value.strip()
|
||||
if new_diary_name and new_diary_name != self.current_diary_name:
|
||||
self.dismiss((self.diary_id, new_diary_name))
|
||||
elif new_diary_name == self.current_diary_name:
|
||||
self.notify("No changes made.", severity="warning")
|
||||
self.dismiss(None)
|
||||
else:
|
||||
self.notify("Diary name cannot be empty.", severity="warning")
|
||||
self.name_input.focus()
|
||||
self.action_edit_diary()
|
||||
elif event.button.id == "cancel_button":
|
||||
self.dismiss(None)
|
||||
def on_key(self, event):
|
||||
"""
|
||||
Handles the Enter key press event by triggering the diary save action and preventing the default behavior.
|
||||
"""
|
||||
if event.key == "enter":
|
||||
self.action_edit_diary()
|
||||
event.prevent_default()
|
||||
|
||||
def action_edit_diary(self) -> None:
|
||||
"""
|
||||
Attempts to save the edited diary name, validating input and providing user feedback.
|
||||
|
||||
If the new name is non-empty and different from the current name, dismisses the modal and returns the diary ID and new name. If the name is unchanged, notifies the user and dismisses without changes. If the input is empty, notifies the user and refocuses the input field.
|
||||
"""
|
||||
new_diary_name = self.name_input.value.strip()
|
||||
if new_diary_name and new_diary_name != self.current_diary_name:
|
||||
self.dismiss((self.diary_id, new_diary_name))
|
||||
elif new_diary_name == self.current_diary_name:
|
||||
self.notify("No changes made.", severity="warning")
|
||||
self.dismiss(None)
|
||||
else:
|
||||
self.notify("Diary name cannot be empty.", severity="warning")
|
||||
self.name_input.focus()
|
||||
|
||||
def on_input_submitted(self, event: Input.Submitted) -> None:
|
||||
"""
|
||||
Handles submission of the diary name input field by triggering the save action if the relevant input is submitted.
|
||||
"""
|
||||
if event.input.id == "edit_diary_name_input":
|
||||
self.action_edit_diary()
|
||||
|
||||
def action_cancel(self) -> None:
|
||||
"""
|
||||
Dismisses the modal without saving changes or returning any data.
|
||||
"""
|
||||
self.dismiss(None)
|
||||
@@ -24,27 +24,33 @@ class EditEntryScreen(Screen):
|
||||
BINDINGS = [
|
||||
Binding("ctrl+q", "quit", "Quit"),
|
||||
Binding("ctrl+s", "save", "Save"),
|
||||
Binding("ctrl+n", "new_entry", "New Entry"),
|
||||
Binding("ctrl+shift+n", "next_entry", "Next Entry"),
|
||||
Binding("ctrl+shift+p", "prev_entry", "Previous Entry"),
|
||||
Binding("shift+f5", "new_entry", "New Entry"),
|
||||
Binding("f5", "next_entry", "Next Entry"),
|
||||
Binding("f4", "prev_entry", "Previous Entry"),
|
||||
Binding("ctrl+r", "rename_entry", "Rename Entry"),
|
||||
Binding("f8", "toggle_sidebar", "Toggle Photos"),
|
||||
Binding("f9", "toggle_focus", "Toggle Focus"),
|
||||
Binding("escape", "back_to_list", "Back to List"),
|
||||
]
|
||||
|
||||
def __init__(self, diary_id: int = 1):
|
||||
print("DEBUG: EditEntryScreen INIT")
|
||||
def __init__(self, diary_id: int = 1,create_new: bool = True):
|
||||
super().__init__()
|
||||
|
||||
if create_new:
|
||||
self.current_entry_index = -1
|
||||
self.is_new_entry = True
|
||||
self.next_entry_id = None
|
||||
|
||||
else:
|
||||
self.is_new_entry = False
|
||||
self.current_entry_index = 0
|
||||
self.next_entry_id = 1
|
||||
self.new_entry_title = ""
|
||||
self.new_entry_content = ""
|
||||
self.diary_id = diary_id
|
||||
self.diary_name = f"Diary {diary_id}"
|
||||
self.current_entry_index = 0
|
||||
self.entries: List[Entry] = []
|
||||
self.is_new_entry = False
|
||||
self.has_unsaved_changes = False
|
||||
self.new_entry_content = ""
|
||||
self.new_entry_title = "New Entry"
|
||||
self.next_entry_id = 1
|
||||
self._updating_display = False
|
||||
self._original_content = ""
|
||||
self.is_refreshing = False
|
||||
@@ -136,7 +142,6 @@ class EditEntryScreen(Screen):
|
||||
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
print("DEBUG: EditEntryScreen COMPOSE", getattr(self, 'sidebar_visible', None))
|
||||
yield self.header
|
||||
yield Horizontal(
|
||||
self.main,
|
||||
@@ -233,7 +238,10 @@ class EditEntryScreen(Screen):
|
||||
current_entry = self.entries[self.current_entry_index]
|
||||
entry_text = f"Entry: \\[{self.current_entry_index + 1}/{len(self.entries)}] {current_entry.title}"
|
||||
self.entry_info.update(entry_text)
|
||||
self._update_status_indicator("Saved", "saved")
|
||||
if self.has_unsaved_changes:
|
||||
self._update_status_indicator("Not Saved", "not-saved")
|
||||
else:
|
||||
self._update_status_indicator("Saved", "saved")
|
||||
|
||||
def _save_current_state(self):
|
||||
"""Saves the current state before navigating"""
|
||||
@@ -339,7 +347,6 @@ class EditEntryScreen(Screen):
|
||||
def action_toggle_sidebar(self):
|
||||
"""Toggles the sidebar visibility"""
|
||||
try:
|
||||
print("DEBUG: TOGGLE SIDEBAR", self.sidebar_visible)
|
||||
self.sidebar_visible = not self.sidebar_visible
|
||||
|
||||
if self.sidebar_visible:
|
||||
@@ -348,13 +355,7 @@ class EditEntryScreen(Screen):
|
||||
# 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
|
||||
self._sidebar_opened_once = True
|
||||
else:
|
||||
self.sidebar.display = False
|
||||
self.sidebar_focused = False # Reset focus when hiding
|
||||
@@ -372,15 +373,12 @@ class EditEntryScreen(Screen):
|
||||
|
||||
def action_toggle_focus(self):
|
||||
"""Toggles focus between editor and sidebar"""
|
||||
print("DEBUG: TOGGLE FOCUS called", self.sidebar_visible, self.sidebar_focused)
|
||||
if not self.sidebar_visible:
|
||||
# If sidebar is not visible, show it and focus it
|
||||
print("DEBUG: Sidebar not visible, opening it")
|
||||
self.action_toggle_sidebar()
|
||||
return
|
||||
|
||||
self.sidebar_focused = not self.sidebar_focused
|
||||
print("DEBUG: Sidebar focused changed to", self.sidebar_focused)
|
||||
if self.sidebar_focused:
|
||||
self.photo_list.focus()
|
||||
else:
|
||||
@@ -757,24 +755,34 @@ class EditEntryScreen(Screen):
|
||||
self.photo_info.update(photo_details)
|
||||
|
||||
def on_text_area_changed(self, event) -> None:
|
||||
"""Detects text changes and shows photo tooltips"""
|
||||
if (hasattr(self, 'text_entry') and not self.text_entry.read_only and
|
||||
not getattr(self, '_updating_display', False) and hasattr(self, '_original_content')):
|
||||
current_content = self.text_entry.text
|
||||
"""Detects text changes and updates status"""
|
||||
# Skip if we're currently updating the display
|
||||
if getattr(self, '_updating_display', False):
|
||||
return
|
||||
|
||||
# Skip if text area is read-only
|
||||
if not hasattr(self, 'text_entry') or self.text_entry.read_only:
|
||||
return
|
||||
|
||||
# Skip if we don't have original content to compare against
|
||||
if not hasattr(self, '_original_content'):
|
||||
return
|
||||
|
||||
current_content = self.text_entry.text
|
||||
|
||||
# Check if content has changed
|
||||
if current_content != self._original_content:
|
||||
if not self.has_unsaved_changes:
|
||||
self.has_unsaved_changes = True
|
||||
self._update_sub_header()
|
||||
else:
|
||||
if self.has_unsaved_changes:
|
||||
self.has_unsaved_changes = False
|
||||
self._update_sub_header()
|
||||
|
||||
|
||||
|
||||
# Check for a photo reference pattern
|
||||
# self._check_photo_reference(current_content) # Temporarily disabled
|
||||
|
||||
if current_content != self._original_content:
|
||||
if not self.has_unsaved_changes:
|
||||
self.has_unsaved_changes = True
|
||||
self._update_sub_header()
|
||||
else:
|
||||
if self.has_unsaved_changes:
|
||||
self.has_unsaved_changes = False
|
||||
self._update_sub_header()
|
||||
|
||||
def on_focus(self, event) -> None:
|
||||
"""Captures focus changes to update footer"""
|
||||
@@ -905,11 +913,22 @@ class EditEntryScreen(Screen):
|
||||
self.notify("Empty entry cannot be saved")
|
||||
return
|
||||
# Passe a lista de fotos para o método de criação
|
||||
self.call_later(self._async_create_entry, content, photos_to_link)
|
||||
if self.new_entry_title == "":
|
||||
self.app.push_screen(RenameEntryModal(current_name=""), lambda result: self._handle_save_after_rename(result,content,
|
||||
photos_to_link))
|
||||
else:
|
||||
self.call_later(self._async_create_entry, content, photos_to_link)
|
||||
else:
|
||||
# Passe a lista de fotos para o método de atualização
|
||||
self.call_later(self._async_update_entry, content, photos_to_link)
|
||||
|
||||
def _handle_save_after_rename(self, result: str | None, content: str, photos_to_link: List[Photo]) -> None:
|
||||
if result is None:
|
||||
self.notify("Save cancelled")
|
||||
return
|
||||
self.new_entry_title = result
|
||||
self.call_later(self._async_create_entry, content, photos_to_link)
|
||||
|
||||
async def _async_create_entry(self, content: str, photos_to_link: List[Photo]):
|
||||
"""Creates a new entry and links the referenced photos."""
|
||||
try:
|
||||
@@ -936,7 +955,7 @@ class EditEntryScreen(Screen):
|
||||
self.is_new_entry = False
|
||||
self.has_unsaved_changes = False
|
||||
self._original_content = new_entry.text
|
||||
self.new_entry_title = "New Entry"
|
||||
self.new_entry_title = ""
|
||||
self.next_entry_id = max(entry.id for entry in self.entries) + 1
|
||||
|
||||
self._update_entry_display()
|
||||
@@ -981,50 +1000,66 @@ class EditEntryScreen(Screen):
|
||||
except Exception as e:
|
||||
self.notify(f"Error updating entry: {str(e)}")
|
||||
|
||||
def on_key(self, event):
|
||||
def check_key(self, event):
|
||||
"""Check for custom key handling before bindings are processed"""
|
||||
|
||||
# Sidebar contextual shortcuts
|
||||
# Sidebar shortcuts
|
||||
if self.sidebar_focused and self.sidebar_visible:
|
||||
sidebar_keys = ["i", "n", "d", "e"]
|
||||
if event.key in sidebar_keys:
|
||||
if event.key == "i":
|
||||
self.action_insert_photo()
|
||||
elif event.key == "n":
|
||||
self.action_ingest_new_photo()
|
||||
elif event.key == "d":
|
||||
self.action_delete_photo()
|
||||
elif event.key == "e":
|
||||
self.action_edit_photo()
|
||||
return True # Indica que o evento foi processado
|
||||
|
||||
if event.key == "i":
|
||||
# Text area shortcuts
|
||||
elif self.focused is self.text_entry:
|
||||
if event.key in ["tab", "shift+tab"]:
|
||||
if event.key == "shift+tab":
|
||||
self._handle_shift_tab()
|
||||
elif event.key == "tab":
|
||||
self.text_entry.insert('\t')
|
||||
return True # Indica que o evento foi processado
|
||||
|
||||
self.action_insert_photo()
|
||||
event.stop()
|
||||
elif event.key == "n":
|
||||
return False # Não foi processado, continuar com bindings
|
||||
|
||||
self.action_ingest_new_photo()
|
||||
event.stop()
|
||||
elif event.key == "d":
|
||||
def _handle_shift_tab(self):
|
||||
"""Handle shift+tab for removing indentation"""
|
||||
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))
|
||||
|
||||
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))
|
||||
def on_key(self, event):
|
||||
if self.check_key(event):
|
||||
event.stop()
|
||||
# Tab: insert tab
|
||||
elif self.focused is self.text_entry and event.key == "tab":
|
||||
self.text_entry.insert('\t')
|
||||
event.stop()
|
||||
return
|
||||
|
||||
def on_footer_action(self, event) -> None:
|
||||
"""Handle clicks on footer actions (Textual 3.x)."""
|
||||
action = event.action
|
||||
method = getattr(self, f"action_{action}", None)
|
||||
if method:
|
||||
method()
|
||||
else:
|
||||
self.notify(f"No action found for: {action}", severity="warning")
|
||||
@@ -4,17 +4,33 @@ from textual.containers import Vertical, Horizontal
|
||||
from textual.screen import ModalScreen
|
||||
from textual.widgets import Label, Input, Button
|
||||
|
||||
from pilgrim.ui.screens.edit_entry_screen import EditEntryScreen
|
||||
|
||||
|
||||
class NewDiaryModal(ModalScreen[str]):
|
||||
BINDINGS = [
|
||||
Binding("escape", "cancel", "Cancel"),
|
||||
Binding("enter", "create_diary", "Create",priority=True),
|
||||
]
|
||||
def __init__(self):
|
||||
def __init__(self,autoopen=True):
|
||||
"""
|
||||
Initialize the NewDiaryModal with optional automatic opening of the created diary.
|
||||
|
||||
Parameters:
|
||||
autoopen (bool): If True, the newly created diary will open automatically after creation. Defaults to True.
|
||||
"""
|
||||
super().__init__()
|
||||
self.auto_open = autoopen
|
||||
self.name_input = Input(id="NewDiaryModal-NameInput",classes="NewDiaryModal-NameInput") # This ID is fine, it's specific to the input
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
|
||||
"""
|
||||
Constructs and yields the UI layout for the new diary modal dialog.
|
||||
|
||||
Returns:
|
||||
ComposeResult: An iterable of widgets forming the modal, including title, input field, and action buttons.
|
||||
"""
|
||||
with Vertical(id="new_diary_dialog",classes="NewDiaryModal-Dialog"):
|
||||
yield Label("Create a new diary", classes="NewDiaryModal-Title")
|
||||
yield Label("Diary Name:")
|
||||
@@ -29,17 +45,67 @@ class NewDiaryModal(ModalScreen[str]):
|
||||
self.name_input.focus()
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
"""Handles button clicks."""
|
||||
"""
|
||||
Handle button press events for the modal.
|
||||
|
||||
Triggers diary creation when the "Create" button is pressed, or dismisses the modal when the "Cancel" button is pressed.
|
||||
"""
|
||||
if event.button.id == "create_diary_button":
|
||||
diary_name = self.name_input.value.strip()
|
||||
if diary_name:
|
||||
self.dismiss(diary_name)
|
||||
else:
|
||||
self.notify("Diary name cannot be empty.", severity="warning")
|
||||
self.name_input.focus()
|
||||
self.action_create_diary()
|
||||
elif event.button.id == "cancel_button":
|
||||
self.dismiss("")
|
||||
self.dismiss()
|
||||
|
||||
def action_cancel(self) -> None:
|
||||
"""Action to cancel the modal."""
|
||||
self.dismiss("")
|
||||
"""
|
||||
Cancels the modal dialog and dismisses it without returning a diary name.
|
||||
"""
|
||||
self.dismiss("")
|
||||
|
||||
def action_create_diary(self) -> None:
|
||||
"""
|
||||
Validates the diary name input and initiates asynchronous diary creation if valid.
|
||||
|
||||
If the input is empty, displays a warning notification and refocuses the input field.
|
||||
"""
|
||||
diary_name = self.name_input.value.strip()
|
||||
if diary_name:
|
||||
|
||||
self.call_later(self._async_create_diary, diary_name)
|
||||
|
||||
else:
|
||||
self.notify("Diary name cannot be empty.", severity="warning")
|
||||
self.name_input.focus()
|
||||
|
||||
def on_input_submitted(self, event: Input.Submitted) -> None:
|
||||
"""
|
||||
Handles submission of the diary name input field and initiates diary creation if triggered from the correct input.
|
||||
"""
|
||||
if event.input.id == "NewDiaryModal-NameInput":
|
||||
self.action_create_diary()
|
||||
|
||||
async def _async_create_diary(self, name: str):
|
||||
|
||||
"""
|
||||
Asynchronously creates a new diary entry with the given name and handles post-creation actions.
|
||||
|
||||
If creation succeeds, dismisses the modal with the diary name, optionally opens the diary for editing, and shows a success notification. If creation fails or an exception occurs, displays an error notification.
|
||||
"""
|
||||
try:
|
||||
service = self.app.service_manager.get_travel_diary_service()
|
||||
created_diary = await service.async_create(name)
|
||||
if created_diary:
|
||||
self.dismiss(name)
|
||||
|
||||
if self.auto_open:
|
||||
self.app.push_screen(EditEntryScreen(diary_id=created_diary.id))
|
||||
|
||||
self.notify(f"Diary: '{name}' created!")
|
||||
else:
|
||||
self.notify("Error Creating the diary")
|
||||
except Exception as e:
|
||||
self.notify(f"Exception on creating the diary: {str(e)}")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user