mirror of https://github.com/gmbrax/Pilgrim.git
				
				
				
			Merge pull request #3 from gmbrax/feat-TUI
Feat TUI Merge the changes so far to start integrating with the database
This commit is contained in:
		
						commit
						37fa9b929b
					
				|  | @ -1,2 +1,3 @@ | |||
| database.db | ||||
| __pycache__ | ||||
| /.idea/ | ||||
|  |  | |||
|  | @ -6,10 +6,8 @@ | |||
|   <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/models/entry.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/pilgrim/models/entry.py" afterDir="false" /> | ||||
|       <change beforePath="$PROJECT_DIR$/src/pilgrim/models/photo.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/pilgrim/models/photo.py" afterDir="false" /> | ||||
|       <change beforePath="$PROJECT_DIR$/src/pilgrim/models/photo_in_entry.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/pilgrim/models/photo_in_entry.py" afterDir="false" /> | ||||
|       <change beforePath="$PROJECT_DIR$/src/pilgrim/service/entry_service.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/pilgrim/service/entry_service.py" 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" /> | ||||
|  | @ -52,24 +50,24 @@ | |||
|     <option name="hideEmptyMiddlePackages" value="true" /> | ||||
|     <option name="showLibraryContents" value="true" /> | ||||
|   </component> | ||||
|   <component name="PropertiesComponent">{ | ||||
|   "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": "master", | ||||
|     "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 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> | ||||
|   <component name="RunManager"> | ||||
|     <configuration name="pilgrim" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true"> | ||||
|       <module name="Pilgrim" /> | ||||
|  | @ -117,6 +115,7 @@ | |||
|       <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" /> | ||||
|  |  | |||
|  | @ -18,7 +18,9 @@ classifiers = [ | |||
|     "Operating System :: OS Independent", | ||||
| ] | ||||
| dependencies = [ | ||||
|   "sqlalchemy" | ||||
|   "sqlalchemy", | ||||
|     "textual", | ||||
|     "textual-dev" | ||||
| ] | ||||
| [template.plugins.default] | ||||
| src-layout = true | ||||
|  |  | |||
|  | @ -2,3 +2,5 @@ greenlet==3.2.3 | |||
| SQLAlchemy==2.0.41 | ||||
| typing_extensions==4.14.0 | ||||
| 
 | ||||
| 
 | ||||
| textual~=3.3.0 | ||||
|  |  | |||
|  | @ -1,13 +1,17 @@ | |||
| from pilgrim.database import Database | ||||
| from pilgrim.service.mocks.service_manager_mock import ServiceManagerMock | ||||
| from pilgrim.service.servicemanager import ServiceManager | ||||
| from pilgrim.ui.ui import UIApp | ||||
| 
 | ||||
| 
 | ||||
| class Application: | ||||
|     def __init__(self): | ||||
|         self.database = Database() | ||||
|         self.ui = UIApp(ServiceManagerMock()) | ||||
| 
 | ||||
|     def run(self): | ||||
|         self.database.create() | ||||
|         self.ui.run() | ||||
| 
 | ||||
|     def get_service_manager(self): | ||||
|         session = self.database.session() | ||||
|  |  | |||
|  | @ -4,3 +4,6 @@ from pilgrim.application import Application | |||
| def main(): | ||||
|     app = Application() | ||||
|     app.run() | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
|     main() | ||||
|  |  | |||
|  | @ -0,0 +1,123 @@ | |||
| from typing import List, Tuple | ||||
| import asyncio | ||||
| from pilgrim.service.entry_service import EntryService | ||||
| from pilgrim.models.entry import Entry | ||||
| 
 | ||||
| 
 | ||||
| class EntryServiceMock(EntryService): | ||||
|     def __init__(self): | ||||
|         super().__init__(None) | ||||
| 
 | ||||
|         self.mock_data = { | ||||
|             1: Entry(title="The Adventure Begins", text="I'm hopping in the Plane to finally visit canadian lands", | ||||
|                      date="26/07/2025", travel_diary_id=1, id=1, | ||||
|                      photos=[]), | ||||
|             2: Entry(title="The Landing", text="Finally on Canadian Soil", date="27/07/2025", | ||||
|                      travel_diary_id=1, id=2,photos=[]), | ||||
|             3: Entry(title="The Mount Royal", text="The Mount Royal is fucking awesome", date="28/07/2025", | ||||
|                      travel_diary_id=1, id=3, photos=[]), | ||||
|             4: Entry(title="Old Montreal", text="Exploring the historic district", date="29/07/2025", | ||||
|                      travel_diary_id=1, id=4, photos=[]), | ||||
|             5: Entry(title="Notre-Dame Basilica", text="Beautiful architecture", date="30/07/2025", | ||||
|                      travel_diary_id=1, id=5, photos=[]), | ||||
|             6: Entry(title="Parc Jean-Drapeau", text="Great views of the city", date="31/07/2025", | ||||
|                      travel_diary_id=1, id=6, photos=[]), | ||||
|             7: Entry(title="La Ronde", text="Amusement park fun", date="01/08/2025", | ||||
|                      travel_diary_id=1, id=7, photos=[]), | ||||
|             8: Entry(title="Biodome", text="Nature and science", date="02/08/2025", | ||||
|                      travel_diary_id=1, id=8, photos=[]), | ||||
|             9: Entry(title="Botanical Gardens", text="Peaceful walk", date="03/08/2025", | ||||
|                      travel_diary_id=1, id=9, photos=[]), | ||||
|             10: Entry(title="Olympic Stadium", text="Historic venue", date="04/08/2025", | ||||
|                      travel_diary_id=1, id=10, photos=[]), | ||||
|         } | ||||
|         self._next_id = 11 | ||||
| 
 | ||||
|     # Métodos síncronos (mantidos para compatibilidade) | ||||
|     def create(self, travel_diary_id: int, title: str, text: str, date: str) -> Entry: | ||||
|         """Versão síncrona""" | ||||
|         new_entry = Entry(title, text, date, travel_diary_id, id=self._next_id) | ||||
|         self.mock_data[self._next_id] = new_entry | ||||
|         self._next_id += 1 | ||||
|         return new_entry | ||||
| 
 | ||||
|     def read_by_id(self, entry_id: int) -> Entry | None: | ||||
|         """Versão síncrona""" | ||||
|         return self.mock_data.get(entry_id) | ||||
| 
 | ||||
|     def read_all(self) -> List[Entry]: | ||||
|         """Versão síncrona""" | ||||
|         return list(self.mock_data.values()) | ||||
| 
 | ||||
|     def read_by_travel_diary_id(self, travel_diary_id: int) -> List[Entry]: | ||||
|         """Versão síncrona - lê entradas por diário""" | ||||
|         return [entry for entry in self.mock_data.values() if entry.fk_travel_diary_id == travel_diary_id] | ||||
| 
 | ||||
|     def read_paginated(self, travel_diary_id: int, page: int = 1, page_size: int = 5) -> Tuple[List[Entry], int, int]: | ||||
|         """Versão síncrona - lê entradas paginadas por diário""" | ||||
|         entries = self.read_by_travel_diary_id(travel_diary_id) | ||||
|         entries.sort(key=lambda x: x.id, reverse=True)  # Mais recentes primeiro | ||||
|          | ||||
|         total_entries = len(entries) | ||||
|         total_pages = (total_entries + page_size - 1) // page_size | ||||
|          | ||||
|         start_index = (page - 1) * page_size | ||||
|         end_index = start_index + page_size | ||||
|          | ||||
|         page_entries = entries[start_index:end_index] | ||||
|          | ||||
|         return page_entries, total_pages, total_entries | ||||
| 
 | ||||
|     def update(self, entry_src: Entry, entry_dst: Entry) -> Entry | None: | ||||
|         """Versão síncrona""" | ||||
|         item_to_update = self.mock_data.get(entry_src.id) | ||||
|         if item_to_update: | ||||
|             item_to_update.title = entry_dst.title if entry_dst.title is not None else item_to_update.title | ||||
|             item_to_update.text = entry_dst.text if entry_dst.text is not None else item_to_update.text | ||||
|             item_to_update.date = entry_dst.date if entry_dst.date is not None else item_to_update.date | ||||
|             item_to_update.fk_travel_diary_id = entry_dst.fk_travel_diary_id if (entry_dst.fk_travel_diary_id | ||||
|                                                                                  is not None) else entry_dst.id | ||||
|             item_to_update.photos.extend(entry_dst.photos) | ||||
| 
 | ||||
|             return item_to_update | ||||
|         return None | ||||
| 
 | ||||
|     def delete(self, entry_src: Entry) -> Entry | None: | ||||
|         """Versão síncrona""" | ||||
|         return self.mock_data.pop(entry_src.id, None) | ||||
| 
 | ||||
|     # Métodos assíncronos (principais) | ||||
|     async def async_create(self, travel_diary_id: int, title: str, text: str, date: str) -> Entry: | ||||
|         """Versão assíncrona""" | ||||
|         await asyncio.sleep(0.01)  # Simula I/O | ||||
|         return self.create(travel_diary_id, title, text, date) | ||||
| 
 | ||||
|     async def async_read_by_id(self, entry_id: int) -> Entry | None: | ||||
|         """Versão assíncrona""" | ||||
|         await asyncio.sleep(0.01)  # Simula I/O | ||||
|         return self.read_by_id(entry_id) | ||||
| 
 | ||||
|     async def async_read_all(self) -> List[Entry]: | ||||
|         """Versão assíncrona""" | ||||
|         await asyncio.sleep(0.01)  # Simula I/O | ||||
|         return self.read_all() | ||||
| 
 | ||||
|     async def async_read_by_travel_diary_id(self, travel_diary_id: int) -> List[Entry]: | ||||
|         """Versão assíncrona - lê entradas por diário""" | ||||
|         await asyncio.sleep(0.01)  # Simula I/O | ||||
|         return self.read_by_travel_diary_id(travel_diary_id) | ||||
| 
 | ||||
|     async def async_read_paginated(self, travel_diary_id: int, page: int = 1, page_size: int = 5) -> Tuple[List[Entry], int, int]: | ||||
|         """Versão assíncrona - lê entradas paginadas por diário""" | ||||
|         await asyncio.sleep(0.01)  # Simula I/O | ||||
|         return self.read_paginated(travel_diary_id, page, page_size) | ||||
| 
 | ||||
|     async def async_update(self, entry_src: Entry, entry_dst: Entry) -> Entry | None: | ||||
|         """Versão assíncrona""" | ||||
|         await asyncio.sleep(0.01)  # Simula I/O | ||||
|         return self.update(entry_src, entry_dst) | ||||
| 
 | ||||
|     async def async_delete(self, entry_src: Entry) -> Entry | None: | ||||
|         """Versão assíncrona""" | ||||
|         await asyncio.sleep(0.01)  # Simula I/O | ||||
|         return self.delete(entry_src) | ||||
|  | @ -0,0 +1,42 @@ | |||
| from pathlib import Path | ||||
| from typing import List | ||||
| 
 | ||||
| from pilgrim.models.photo import Photo | ||||
| from pilgrim.service.photo_service import PhotoService | ||||
| 
 | ||||
| 
 | ||||
| class PhotoServiceMock(PhotoService): | ||||
|     def __init__(self): | ||||
|         super().__init__(None) | ||||
|         self.mock_data = {} | ||||
|         self._next_id = 1 | ||||
| 
 | ||||
|     def create(self, filepath: Path, name: str, travel_diary_id, addition_date=None, caption=None) -> Photo | None: | ||||
|         new_photo = Photo(filepath, name, addition_date=addition_date, caption=caption) | ||||
|         self.mock_data[self._next_id] = new_photo | ||||
|         self._next_id += 1 | ||||
|         return new_photo | ||||
| 
 | ||||
| 
 | ||||
|     def read_by_id(self, photo_id: int) -> Photo: | ||||
|         return self.mock_data.get(photo_id) | ||||
| 
 | ||||
|     def read_all(self) -> List[Photo]: | ||||
|         return list(self.mock_data.values()) | ||||
| 
 | ||||
|     def update(self, photo_id: Photo, photo_dst: Photo) -> Photo | None: | ||||
|         item_to_update:Photo = self.mock_data.get(photo_id) | ||||
|         if item_to_update: | ||||
|             item_to_update.filepath = photo_dst.filepath if photo_dst.filepath else item_to_update.filepath | ||||
|             item_to_update.name = photo_dst.name if photo_dst.name else item_to_update.name | ||||
|             item_to_update.caption = photo_dst.caption if photo_dst.caption else item_to_update.caption | ||||
|             item_to_update.addition_date = photo_dst.addition_date if photo_dst.addition_date\ | ||||
|                 else item_to_update.addition_date | ||||
|             item_to_update.fk_travel_diary_id = photo_dst.fk_travel_diary_id if photo_dst.fk_travel_diary_id \ | ||||
|                 else item_to_update.fk_travel_diary_id | ||||
|             item_to_update.entries.extend(photo_dst.entries) | ||||
|             return item_to_update | ||||
|         return None | ||||
| 
 | ||||
|     def delete(self, photo_id: int) -> Photo | None: | ||||
|         return self.mock_data.pop(photo_id, None) | ||||
|  | @ -0,0 +1,23 @@ | |||
| from pilgrim.service.mocks.entry_service_mock import EntryServiceMock | ||||
| from pilgrim.service.mocks.photo_service_mock import PhotoServiceMock | ||||
| from pilgrim.service.mocks.travel_diary_service_mock import TravelDiaryServiceMock | ||||
| from pilgrim.service.photo_service import PhotoService | ||||
| from pilgrim.service.servicemanager import ServiceManager | ||||
| 
 | ||||
| 
 | ||||
| class ServiceManagerMock(ServiceManager): | ||||
|     def __init__(self): | ||||
|         super().__init__() | ||||
|         # Cria instâncias únicas para manter estado consistente | ||||
|         self._travel_diary_service = TravelDiaryServiceMock() | ||||
|         self._entry_service = EntryServiceMock() | ||||
|         self._photo_service = PhotoServiceMock() | ||||
| 
 | ||||
|     def get_entry_service(self): | ||||
|         return self._entry_service | ||||
| 
 | ||||
|     def get_travel_diary_service(self): | ||||
|         return self._travel_diary_service | ||||
| 
 | ||||
|     def get_photo_service(self): | ||||
|         return self._photo_service | ||||
|  | @ -0,0 +1,67 @@ | |||
| from pilgrim.service.travel_diary_service import TravelDiaryService | ||||
| from pilgrim.models.travel_diary import TravelDiary | ||||
| import asyncio | ||||
| 
 | ||||
| 
 | ||||
| class TravelDiaryServiceMock(TravelDiaryService): | ||||
|     def __init__(self): | ||||
|         super().__init__(None) | ||||
|         self.mock_data = { | ||||
|             1: TravelDiary(id=1, name="Montreal"), | ||||
|             2: TravelDiary(id=2, name="Rio de Janeiro"), | ||||
|         } | ||||
|         self._next_id = 3 | ||||
| 
 | ||||
|     # Métodos síncronos (originais) | ||||
|     def create(self, name: str): | ||||
|         """Versão síncrona""" | ||||
|         new_travel_diary = TravelDiary(id=self._next_id, name=name) | ||||
|         self.mock_data[self._next_id] = new_travel_diary | ||||
|         self._next_id += 1 | ||||
|         return new_travel_diary | ||||
| 
 | ||||
|     def read_by_id(self, travel_id: int): | ||||
|         """Versão síncrona""" | ||||
|         return self.mock_data.get(travel_id) | ||||
| 
 | ||||
|     def read_all(self): | ||||
|         """Versão síncrona""" | ||||
|         return list(self.mock_data.values()) | ||||
| 
 | ||||
|     def update(self, travel_diary_id: int, name: str): | ||||
|         """Versão síncrona""" | ||||
|         item_to_update = self.mock_data.get(travel_diary_id) | ||||
|         if item_to_update: | ||||
|             item_to_update.name = name | ||||
|             return item_to_update | ||||
|         return None | ||||
| 
 | ||||
|     def delete(self, travel_diary_id: int): | ||||
|         """Versão síncrona""" | ||||
|         return self.mock_data.pop(travel_diary_id, None) | ||||
| 
 | ||||
|     # Métodos assíncronos (novos) | ||||
|     async def async_create(self, name: str): | ||||
|         """Versão assíncrona""" | ||||
|         await asyncio.sleep(0.01)  # Simula I/O | ||||
|         return self.create(name) | ||||
| 
 | ||||
|     async def async_read_by_id(self, travel_id: int): | ||||
|         """Versão assíncrona""" | ||||
|         await asyncio.sleep(0.01)  # Simula I/O | ||||
|         return self.read_by_id(travel_id) | ||||
| 
 | ||||
|     async def async_read_all(self): | ||||
|         """Versão assíncrona""" | ||||
|         await asyncio.sleep(0.01)  # Simula I/O | ||||
|         return self.read_all() | ||||
| 
 | ||||
|     async def async_update(self, travel_diary_id: int, name: str): | ||||
|         """Versão assíncrona""" | ||||
|         await asyncio.sleep(0.01)  # Simula I/O | ||||
|         return self.update(travel_diary_id, name) | ||||
| 
 | ||||
|     async def async_delete(self, travel_diary_id: int): | ||||
|         """Versão assíncrona""" | ||||
|         await asyncio.sleep(0.01)  # Simula I/O | ||||
|         return self.delete(travel_diary_id) | ||||
|  | @ -1,8 +1,9 @@ | |||
| from pathlib import Path | ||||
| from typing import List | ||||
| 
 | ||||
| from pilgrim import Photo, TravelDiary | ||||
| 
 | ||||
| from pilgrim.models.photo import Photo | ||||
| from pilgrim.models.travel_diary import TravelDiary | ||||
| 
 | ||||
| class PhotoService: | ||||
|     def __init__(self, session): | ||||
|  |  | |||
|  | @ -18,18 +18,18 @@ class TravelDiaryService: | |||
|     def read_all(self): | ||||
|         return self.session.query(TravelDiary).all() | ||||
| 
 | ||||
|     def update(self, travel_diary_src:TravelDiary,travel_diary_dst:TravelDiary): | ||||
|         original = self.read_by_id(travel_diary_src.id) | ||||
|     def update(self, travel_diary_id: TravelDiary, travel_diary_dst: TravelDiary): | ||||
|         original = self.read_by_id(travel_diary_id.id) | ||||
|         if original is not None: | ||||
|             original.name = travel_diary_dst.name | ||||
|             self.session.commit() | ||||
|             self.session.refresh(original) | ||||
|         return original | ||||
| 
 | ||||
|     def delete(self, travel_diary_src:TravelDiary): | ||||
|         excluded = self.read_by_id(travel_diary_src.id) | ||||
|     def delete(self, travel_diary_id: TravelDiary): | ||||
|         excluded = self.read_by_id(travel_diary_id.id) | ||||
|         if excluded is not None: | ||||
|             self.session.delete(travel_diary_src) | ||||
|             self.session.delete(travel_diary_id) | ||||
|             self.session.commit() | ||||
|             return excluded | ||||
|         return None | ||||
|  |  | |||
|  | @ -0,0 +1,73 @@ | |||
| from textual.app import ComposeResult | ||||
| from textual.binding import Binding | ||||
| from textual.screen import Screen | ||||
| from textual.widgets import Header, Footer, Button, Label, TextArea | ||||
| from textual.containers import Container | ||||
| 
 | ||||
| class AboutScreen(Screen[bool]): | ||||
|     """Tela para exibir informações sobre a aplicação.""" | ||||
| 
 | ||||
|     TITLE = "Pilgrim - About" | ||||
| 
 | ||||
|     BINDINGS = [ | ||||
|         Binding("escape", "dismiss", "Close"), | ||||
|     ] | ||||
| 
 | ||||
|     def __init__(self): | ||||
|         super().__init__() | ||||
|         self.header = Header() | ||||
|         self.footer = Footer() | ||||
|         self.app_title = Label("Pilgrim", id="AboutScreen_AboutTitle",classes="AboutScreen_AboutTitle") | ||||
|         self.content = Label("A TUI Based Travel Diary Application", id="AboutScreen_AboutContent", | ||||
|                              classes="AboutScreen_AboutContent") | ||||
|         self.version = Label("Version: 0.0.1", id="AboutScreen_AboutVersion", | ||||
|                              classes="AboutScreen_AboutVersion") | ||||
|         self.developer = Label("Developed By: Gustavo Henrique Miranda ", id="AboutScreen_AboutAuthor") | ||||
|         self.contact = Label("git.gustavomiranda.xyz", id="AboutScreen_AboutContact", | ||||
|                              classes="AboutScreen_AboutContact") | ||||
|         self.license = TextArea(id="AboutScreen_AboutLicense", | ||||
|                                 classes="AboutScreen_AboutLicense") | ||||
|         self.license.text = """Copyright (c) 2025 GHMiranda.  | ||||
| 
 | ||||
| Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: | ||||
| 
 | ||||
| 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. | ||||
| 
 | ||||
| 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. | ||||
| 
 | ||||
| 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. | ||||
| 
 | ||||
| THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.""" | ||||
|         self.license.read_only = True | ||||
|         self.about_container = Container(self.app_title, self.content, self.version, self.developer, | ||||
|                                          self.contact, id="AboutScreen_SubContainer", | ||||
|                                          classes="AboutScreen_SubContainer") | ||||
|         self.container = Container(self.about_container, self.license, id="AboutScreen_AboutContainer", | ||||
|                                    classes="AboutScreen_AboutContainer") | ||||
| 
 | ||||
|     def compose(self) -> ComposeResult: | ||||
|         yield self.header | ||||
|         yield self.container | ||||
|         yield self.footer | ||||
| 
 | ||||
|     def on_button_pressed(self, event: Button.Pressed) -> None: | ||||
|         """Lida com os cliques dos botões.""" | ||||
|         if "about-close-button" in event.button.classes: | ||||
|             self.dismiss(False) | ||||
|         elif "about-info-button" in event.button.classes: | ||||
|             self.notify("Mais informações seriam exibidas aqui!", title="Info") | ||||
| 
 | ||||
|     def action_dismiss(self, **kwargs) -> None: | ||||
|         """Fecha o about box usando dismiss. | ||||
|         :param **kwargs: | ||||
|         """ | ||||
|         self.dismiss(False) | ||||
| 
 | ||||
|     def on_key(self, event) -> None: | ||||
|         """Intercepta teclas específicas.""" | ||||
|         if event.key == "escape": | ||||
|             self.dismiss(False) | ||||
|             event.prevent_default() | ||||
|         elif event.key == "enter": | ||||
|             self.dismiss(False) | ||||
|             event.prevent_default() | ||||
|  | @ -0,0 +1,295 @@ | |||
| from typing import Optional, Tuple | ||||
| import asyncio | ||||
| 
 | ||||
| from textual.app import ComposeResult | ||||
| from textual.screen import Screen | ||||
| from textual.widgets import Header, Footer, Label, Static, OptionList, Button | ||||
| from textual.binding import Binding | ||||
| from textual.containers import Vertical, Container, Horizontal | ||||
| 
 | ||||
| from pilgrim.models.travel_diary import TravelDiary | ||||
| from pilgrim.ui.screens.about_screen import AboutScreen | ||||
| from pilgrim.ui.screens.edit_diary_modal import EditDiaryModal | ||||
| from pilgrim.ui.screens.new_diary_modal import NewDiaryModal | ||||
| from pilgrim.ui.screens.edit_entry_screen import EditEntryScreen | ||||
| 
 | ||||
| 
 | ||||
| class DiaryListScreen(Screen): | ||||
|     TITLE = "Pilgrim - Main" | ||||
| 
 | ||||
|     BINDINGS = [ | ||||
|         Binding("n", "new_diary", "New diary"), | ||||
|         Binding("^q", "quit", "Quit Pilgrim"), | ||||
|         Binding("enter", "open_selected_diary", "Open diary"), | ||||
|         Binding("e", "edit_selected_diary", "Edit diary"), | ||||
|         Binding("r", "force_refresh", "Force refresh"), | ||||
|     ] | ||||
| 
 | ||||
|     def __init__(self): | ||||
|         super().__init__() | ||||
|         self.selected_diary_index = None | ||||
|         self.diary_id_map = {} | ||||
|         self.is_refreshing = False | ||||
| 
 | ||||
|         self.header = Header() | ||||
|         self.footer = Footer() | ||||
|         self.diary_list = OptionList(classes="DiaryListScreen-DiaryListOptions") | ||||
|         self.new_diary_button = Button("New diary", id="new_diary", classes="DiaryListScreen-NewDiaryButton") | ||||
|         self.edit_diary_button = Button("Edit diary", id="edit_diary", classes="DiaryListScreen-EditDiaryButton") | ||||
|         self.open_diary = Button("Open diary", id="open_diary", classes="DiaryListScreen-OpenDiaryButton") | ||||
|         self.buttons_grid = Horizontal( | ||||
|             self.new_diary_button, self.edit_diary_button, self.open_diary, | ||||
|             classes="DiaryListScreen-ButtonsGrid" | ||||
|         ) | ||||
|         self.tips = Static( | ||||
|             "Tip: use ↑↓ to navigate • ENTER to Select • " | ||||
|             "TAB to alternate the fields • SHIFT + TAB to alternate back • " | ||||
|             "Ctrl+P for command palette • R to force refresh", | ||||
|             classes="DiaryListScreen-DiaryListTips" | ||||
|         ) | ||||
|         self.container = Container( | ||||
|             self.diary_list, self.buttons_grid, self.tips, | ||||
|             classes="DiaryListScreen-DiaryListContainer" | ||||
|         ) | ||||
| 
 | ||||
|     def compose(self) -> ComposeResult: | ||||
|         yield self.header | ||||
|         yield self.container | ||||
|         yield self.footer | ||||
| 
 | ||||
|     def on_mount(self) -> None: | ||||
|         # Usa versão síncrona para o mount inicial | ||||
|         self.refresh_diaries() | ||||
|         self.update_buttons_state() | ||||
| 
 | ||||
|     def refresh_diaries(self): | ||||
|         """Versão síncrona do refresh""" | ||||
|         try: | ||||
|             service_manager = self.app.service_manager | ||||
|             travel_diary_service = service_manager.get_travel_diary_service() | ||||
| 
 | ||||
|             # Usa método síncrono | ||||
|             diaries = travel_diary_service.read_all() | ||||
| 
 | ||||
|             # Salva o estado atual | ||||
|             current_diary_id = None | ||||
|             if (self.selected_diary_index is not None and | ||||
|                     self.selected_diary_index in self.diary_id_map): | ||||
|                 current_diary_id = self.diary_id_map[self.selected_diary_index] | ||||
| 
 | ||||
|             # Limpa e reconstrói | ||||
|             self.diary_list.clear_options() | ||||
|             self.diary_id_map = {} | ||||
| 
 | ||||
|             if not diaries: | ||||
|                 self.diary_list.add_option("[dim]Nenhum diário encontrado. Pressione 'N' para criar um novo![/dim]") | ||||
|                 self.selected_diary_index = None | ||||
|             else: | ||||
|                 new_selected_index = 0 | ||||
| 
 | ||||
|                 for index, diary in enumerate(diaries): | ||||
|                     self.diary_id_map[index] = diary.id | ||||
|                     self.diary_list.add_option(f"[b]{diary.name}[/b]\n[dim]ID: {diary.id}[/dim]") | ||||
| 
 | ||||
|                     # Mantém a seleção se possível | ||||
|                     if current_diary_id and diary.id == current_diary_id: | ||||
|                         new_selected_index = index | ||||
| 
 | ||||
|                 self.selected_diary_index = new_selected_index | ||||
| 
 | ||||
|                 # Atualiza o highlight | ||||
|                 self.set_timer(0.05, lambda: self._update_highlight(new_selected_index)) | ||||
| 
 | ||||
|             # Força refresh visual | ||||
|             self.diary_list.refresh() | ||||
|             self.update_buttons_state() | ||||
| 
 | ||||
|         except Exception as e: | ||||
|             self.notify(f"Erro ao carregar diários: {str(e)}") | ||||
| 
 | ||||
|     def _update_highlight(self, index: int): | ||||
|         """Atualiza o highlight do OptionList""" | ||||
|         try: | ||||
|             if index < len(self.diary_list.options): | ||||
|                 self.diary_list.highlighted = index | ||||
|                 self.diary_list.refresh() | ||||
|         except Exception as e: | ||||
|             self.notify(f"Erro ao atualizar highlight: {str(e)}") | ||||
| 
 | ||||
|     async def async_refresh_diaries(self): | ||||
|         """Versão assíncrona do refresh""" | ||||
|         if self.is_refreshing: | ||||
|             return | ||||
| 
 | ||||
|         self.is_refreshing = True | ||||
| 
 | ||||
|         try: | ||||
|             service_manager = self.app.service_manager | ||||
|             travel_diary_service = service_manager.get_travel_diary_service() | ||||
| 
 | ||||
|             # Usa método assíncrono | ||||
|             diaries = await travel_diary_service.async_read_all() | ||||
| 
 | ||||
|             # Salva o estado atual | ||||
|             current_diary_id = None | ||||
|             if (self.selected_diary_index is not None and | ||||
|                     self.selected_diary_index in self.diary_id_map): | ||||
|                 current_diary_id = self.diary_id_map[self.selected_diary_index] | ||||
| 
 | ||||
|             # Limpa e reconstrói | ||||
|             self.diary_list.clear_options() | ||||
|             self.diary_id_map = {} | ||||
| 
 | ||||
|             if not diaries: | ||||
|                 self.diary_list.add_option("[dim]Nenhum diário encontrado. Pressione 'N' para criar um novo![/dim]") | ||||
|                 self.selected_diary_index = None | ||||
|             else: | ||||
|                 new_selected_index = 0 | ||||
| 
 | ||||
|                 for index, diary in enumerate(diaries): | ||||
|                     self.diary_id_map[index] = diary.id | ||||
|                     self.diary_list.add_option(f"[b]{diary.name}[/b]\n[dim]ID: {diary.id}[/dim]") | ||||
| 
 | ||||
|                     if current_diary_id and diary.id == current_diary_id: | ||||
|                         new_selected_index = index | ||||
| 
 | ||||
|                 self.selected_diary_index = new_selected_index | ||||
|                 self.set_timer(0.05, lambda: self._update_highlight(new_selected_index)) | ||||
| 
 | ||||
|             self.diary_list.refresh() | ||||
|             self.update_buttons_state() | ||||
| 
 | ||||
|         except Exception as e: | ||||
|             self.notify(f"Erro ao carregar diários: {str(e)}") | ||||
|         finally: | ||||
|             self.is_refreshing = False | ||||
| 
 | ||||
|     def on_option_list_option_highlighted(self, event: OptionList.OptionHighlighted) -> None: | ||||
|         """Handle quando uma opção é destacada""" | ||||
|         if self.diary_id_map and event.option_index in self.diary_id_map: | ||||
|             self.selected_diary_index = event.option_index | ||||
|         else: | ||||
|             self.selected_diary_index = None | ||||
| 
 | ||||
|         self.update_buttons_state() | ||||
| 
 | ||||
|     def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None: | ||||
|         """Handle quando uma opção é selecionada""" | ||||
|         if self.diary_id_map and event.option_index in self.diary_id_map: | ||||
|             self.selected_diary_index = event.option_index | ||||
|             self.action_open_diary() | ||||
|         else: | ||||
|             self.selected_diary_index = None | ||||
| 
 | ||||
|         self.update_buttons_state() | ||||
| 
 | ||||
|     def update_buttons_state(self): | ||||
|         """Atualiza o estado dos botões""" | ||||
|         has_selection = (self.selected_diary_index is not None and | ||||
|                          self.selected_diary_index in self.diary_id_map) | ||||
| 
 | ||||
|         self.edit_diary_button.disabled = not has_selection | ||||
|         self.open_diary.disabled = not has_selection | ||||
| 
 | ||||
|     def on_button_pressed(self, event: Button.Pressed) -> None: | ||||
|         """Handle cliques nos botões""" | ||||
|         button_id = event.button.id | ||||
| 
 | ||||
|         if button_id == "new_diary": | ||||
|             self.action_new_diary() | ||||
|         elif button_id == "edit_diary": | ||||
|             self.action_edit_selected_diary() | ||||
|         elif button_id == "open_diary": | ||||
|             self.action_open_diary() | ||||
| 
 | ||||
|     def action_new_diary(self): | ||||
|         """Ação para criar novo diário""" | ||||
|         self.app.push_screen(NewDiaryModal(),self._on_new_diary_submitted) | ||||
| 
 | ||||
|     def _on_new_diary_submitted(self,result): | ||||
|         self.notify(str(result)) | ||||
|         if result: | ||||
|             self.notify(f"Creating Diary:{result}'...") | ||||
|             self.call_later(self._async_create_diary,result) | ||||
|         else: | ||||
|             self.notify(f"Canceled...") | ||||
| 
 | ||||
|     async def _async_create_diary(self,name: str): | ||||
| 
 | ||||
|         try: | ||||
|             service = self.app.service_manager.get_travel_diary_service() | ||||
|             created_diary = await service.async_create(name) | ||||
|             if created_diary: | ||||
|                 self.diary_id_map[created_diary.id] = created_diary.id | ||||
|                 await self.async_refresh_diaries() | ||||
|                 self.notify(f"Diary: '{name}' created!") | ||||
|             else: | ||||
|                 self.notify("Error Creating the diary") | ||||
|         except Exception as e: | ||||
|             self.notify(f"Exception on creating the diary: {str(e)}") | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|     def action_edit_selected_diary(self): | ||||
|         """Ação para editar diário selecionado""" | ||||
|         if self.selected_diary_index is not None: | ||||
|             diary_id = self.diary_id_map.get(self.selected_diary_index) | ||||
|             if diary_id: | ||||
|                 self.app.push_screen( | ||||
|                     EditDiaryModal(diary_id=diary_id), | ||||
|                     self._on_edited_diary_name_submitted | ||||
|                 ) | ||||
|         else: | ||||
|             self.notify("Selecione um diário para editar") | ||||
| 
 | ||||
|     def action_open_diary(self): | ||||
|         """Ação para abrir diário selecionado""" | ||||
|         if self.selected_diary_index is not None: | ||||
|             diary_id = self.diary_id_map.get(self.selected_diary_index) | ||||
|             if diary_id: | ||||
|                 self.app.push_screen(EditEntryScreen(diary_id=diary_id)) | ||||
|                 self.notify(f"Opening diary ID: {diary_id}") | ||||
|             else: | ||||
|                 self.notify("Invalid diary ID") | ||||
|         else: | ||||
|             self.notify("Selecione um diário para abrir") | ||||
| 
 | ||||
|     def _on_edited_diary_name_submitted(self, result: Optional[Tuple[int, str]]) -> None: | ||||
|         """Callback após edição do diário""" | ||||
|         if result: | ||||
|             diary_id, name = result | ||||
|             self.notify(f"Atualizando diário ID {diary_id} para '{name}'...") | ||||
|             # Agenda a atualização assíncrona | ||||
|             self.call_later(self._async_update_diary, diary_id, name) | ||||
|         else: | ||||
|             self.notify("Edição cancelada") | ||||
| 
 | ||||
|     async def _async_update_diary(self, diary_id: int, name: str): | ||||
|         """Atualiza o diário de forma assíncrona""" | ||||
|         try: | ||||
|             service = self.app.service_manager.get_travel_diary_service() | ||||
|             updated_diary = await service.async_update(diary_id, name) | ||||
| 
 | ||||
|             if updated_diary: | ||||
|                 self.notify(f"Diário '{name}' atualizado!") | ||||
|                 # Força refresh após a atualização | ||||
|                 await self.async_refresh_diaries() | ||||
|             else: | ||||
|                 self.notify("Erro: Diário não encontrado") | ||||
| 
 | ||||
|         except Exception as e: | ||||
|             self.notify(f"Erro ao atualizar: {str(e)}") | ||||
| 
 | ||||
|     def action_force_refresh(self): | ||||
|         """Força refresh manual""" | ||||
|         self.notify("Forçando refresh...") | ||||
|         # Tenta ambas as versões | ||||
|         self.refresh_diaries()  # Síncrona | ||||
|         self.call_later(self.async_refresh_diaries)  # Assíncrona | ||||
| 
 | ||||
|     def action_open_selected_diary(self): | ||||
|         """Ação do binding ENTER""" | ||||
|         self.action_open_diary() | ||||
| 
 | ||||
|     def action_about_cmd(self): | ||||
|         self.app.push_screen(AboutScreen()) | ||||
|  | @ -0,0 +1,48 @@ | |||
| from textual.app import ComposeResult | ||||
| from textual.containers import Vertical, Horizontal | ||||
| from textual.screen import ModalScreen | ||||
| from textual.widgets import Label, Input, Button | ||||
| 
 | ||||
| 
 | ||||
| class EditDiaryModal(ModalScreen[tuple[int,str]]): | ||||
|     BINDINGS = [ | ||||
|         ("escape", "cancel", "Cancelar"), | ||||
|     ] | ||||
| 
 | ||||
|     def __init__(self, diary_id: int): | ||||
|         super().__init__() | ||||
|         self.diary_id = diary_id | ||||
|         self.current_diary_name = self.app.service_manager.get_travel_diary_service().read_by_id(self.diary_id).name | ||||
|         self.name_input = Input(value=self.current_diary_name, id="edit_diary_name_input",classes="EditDiaryModal-NameInput") | ||||
| 
 | ||||
|     def compose(self) -> ComposeResult: | ||||
|         with Vertical(id="edit_diary_dialog", classes="EditDiaryModal-Dialog"): | ||||
|             yield Label(f"Editar Diário: {self.current_diary_name}", classes="EditDiaryModal-Title") | ||||
|             yield Label("Novo Nome do Diário:") | ||||
|             yield self.name_input | ||||
|             with Horizontal(classes="dialog-buttons"): | ||||
|                 yield Button("Salvar", variant="primary", id="save_diary_button") | ||||
|                 yield Button("Cancelar", variant="default", id="cancel_button") | ||||
| 
 | ||||
|     def on_mount(self) -> None: | ||||
|         """Foca no campo de entrada e move o cursor para o final do texto.""" | ||||
|         self.name_input.focus() | ||||
|         self.name_input.cursor_position = len(self.name_input.value) | ||||
|         # REMOVIDA A LINHA QUE CAUSA O ERRO: self.name_input.select_text() | ||||
| 
 | ||||
|     def on_button_pressed(self, event: Button.Pressed) -> None: | ||||
|         if event.button.id == "save_diary_button": | ||||
|             new_diary_name = self.name_input.value.strip() | ||||
|             if new_diary_name and new_diary_name != self.current_diary_name: | ||||
|                 self.dismiss((self.diary_id, new_diary_name)) | ||||
|             elif new_diary_name == self.current_diary_name: | ||||
|                 self.notify("Nenhuma alteração feita.", severity="warning") | ||||
|                 self.dismiss(None) | ||||
|             else: | ||||
|                 self.notify("O nome do diário não pode estar vazio.", severity="warning") | ||||
|                 self.name_input.focus() | ||||
|         elif event.button.id == "cancel_button": | ||||
|             self.dismiss(None) | ||||
| 
 | ||||
|     def action_cancel(self) -> None: | ||||
|         self.dismiss(None) | ||||
|  | @ -0,0 +1,439 @@ | |||
| from typing import Optional, List | ||||
| import asyncio | ||||
| from datetime import datetime | ||||
| 
 | ||||
| from textual.app import ComposeResult | ||||
| from textual.screen import Screen | ||||
| from textual.widgets import Header, Footer, Static, TextArea | ||||
| from textual.binding import Binding | ||||
| from textual.containers import Container, Horizontal | ||||
| 
 | ||||
| from pilgrim.models.entry import Entry | ||||
| from pilgrim.models.travel_diary import TravelDiary | ||||
| from pilgrim.ui.screens.rename_entry_modal import RenameEntryModal | ||||
| 
 | ||||
| 
 | ||||
| class EditEntryScreen(Screen): | ||||
|     TITLE = "Pilgrim - Edit Entry" | ||||
| 
 | ||||
|     BINDINGS = [ | ||||
|         Binding("ctrl+s", "save", "Save"), | ||||
|         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("r", "force_refresh", "Force refresh"), | ||||
|     ] | ||||
| 
 | ||||
|     def __init__(self, diary_id: int = 1): | ||||
|         super().__init__() | ||||
|         self.diary_id = diary_id | ||||
|         self.diary_name = "Unknown Diary" | ||||
|         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 | ||||
| 
 | ||||
|         # Main header | ||||
|         self.header = Header(name="Pilgrim v6", classes="EditEntryScreen-header") | ||||
| 
 | ||||
|         # Sub-header widgets | ||||
|         self.diary_info = Static(f"Diary: {self.diary_name}", id="diary_info", classes="EditEntryScreen-diary-info") | ||||
|         self.entry_info = Static("Loading...", id="entry_info", classes="EditEntryScreen-entry-info") | ||||
|         self.status_indicator = Static("Saved", id="status_indicator", classes="EditEntryScreen-status-indicator") | ||||
| 
 | ||||
|         # Sub-header container | ||||
|         self.sub_header = Horizontal( | ||||
|             self.diary_info, | ||||
|             Static(classes="spacer EditEntryScreen-spacer"), | ||||
|             self.entry_info, | ||||
|             self.status_indicator, | ||||
|             id="sub_header", | ||||
|             classes="EditEntryScreen-sub-header" | ||||
|         ) | ||||
| 
 | ||||
|         # Text area | ||||
|         self.text_entry = TextArea(id="text_entry", classes="EditEntryScreen-text-entry") | ||||
| 
 | ||||
|         # Main container | ||||
|         self.main = Container( | ||||
|             self.sub_header, | ||||
|             self.text_entry, | ||||
|             id="EditEntryScreen_MainContainer", | ||||
|             classes="EditEntryScreen-main-container" | ||||
|         ) | ||||
| 
 | ||||
|         # Footer | ||||
|         self.footer = Footer(classes="EditEntryScreen-footer") | ||||
| 
 | ||||
|     def compose(self) -> ComposeResult: | ||||
|         yield self.header | ||||
|         yield self.main | ||||
|         yield self.footer | ||||
| 
 | ||||
|     def on_mount(self) -> None: | ||||
|         """Called when the screen is mounted""" | ||||
|         # First update diary info, then refresh entries | ||||
|         self.update_diary_info() | ||||
|         # Use a small delay to ensure diary info is loaded before refreshing entries | ||||
|         self.set_timer(0.1, self.refresh_entries) | ||||
| 
 | ||||
|     def update_diary_info(self): | ||||
|         """Updates diary information""" | ||||
|         try: | ||||
|             service_manager = self.app.service_manager | ||||
|             travel_diary_service = service_manager.get_travel_diary_service() | ||||
|              | ||||
|             diary = travel_diary_service.read_by_id(self.diary_id) | ||||
|             if diary: | ||||
|                 self.diary_name = diary.name | ||||
|                 self.diary_info.update(f"Diary: {self.diary_name}") | ||||
|                 self.notify(f"Loaded diary: {self.diary_name}") | ||||
|             else: | ||||
|                 self.notify(f"Diary with ID {self.diary_id} not found") | ||||
|         except Exception as e: | ||||
|             self.notify(f"Error loading diary info: {str(e)}") | ||||
| 
 | ||||
|     def refresh_entries(self): | ||||
|         """Synchronous version of refresh""" | ||||
|         try: | ||||
|             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: | ||||
|                 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)}") | ||||
| 
 | ||||
|     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) | ||||
|         self.status_indicator.remove_class("saved", "not-saved", "new", "read-only") | ||||
|         self.status_indicator.add_class(css_class) | ||||
| 
 | ||||
|     def _update_sub_header(self): | ||||
|         """Updates the sub-header with current entry information.""" | ||||
|         if not self.entries and not self.is_new_entry: | ||||
|             self.entry_info.update("No entries") | ||||
|             self._update_status_indicator("Saved", "saved") | ||||
|             return | ||||
| 
 | ||||
|         if self.is_new_entry: | ||||
|             self.entry_info.update(f"New Entry: {self.new_entry_title}") | ||||
|             if self.has_unsaved_changes: | ||||
|                 self._update_status_indicator("Not Saved", "not-saved") | ||||
|             else: | ||||
|                 self._update_status_indicator("New", "new") | ||||
|         else: | ||||
|             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") | ||||
| 
 | ||||
|     def _save_current_state(self): | ||||
|         """Saves the current state before navigating""" | ||||
|         if self.is_new_entry: | ||||
|             self.new_entry_content = self.text_entry.text | ||||
|         elif self.entries and self.has_unsaved_changes: | ||||
|             current_entry = self.entries[self.current_entry_index] | ||||
|             current_entry.text = self.text_entry.text | ||||
| 
 | ||||
|     def _finish_display_update(self): | ||||
|         """Finishes the display update by reactivating change detection""" | ||||
|         self._updating_display = False | ||||
|         self._update_sub_header() | ||||
| 
 | ||||
|     def _update_entry_display(self): | ||||
|         """Updates the display of the current entry""" | ||||
|         if not self.entries and not self.is_new_entry: | ||||
|             # Ensure diary name is loaded | ||||
|             if self.diary_name == "Unknown Diary": | ||||
|                 self.update_diary_info() | ||||
|              | ||||
|             self.text_entry.text = f"No entries found for diary '{self.diary_name}'\n\nPress Ctrl+N to create a new entry." | ||||
|             self.text_entry.read_only = True | ||||
|             self._original_content = self.text_entry.text | ||||
|             self._update_sub_header() | ||||
|             return | ||||
| 
 | ||||
|         self._updating_display = True | ||||
| 
 | ||||
|         if self.is_new_entry: | ||||
|             self.text_entry.text = self.new_entry_content | ||||
|             self.text_entry.read_only = False | ||||
|             self._original_content = self.new_entry_content | ||||
|             self.has_unsaved_changes = False | ||||
|         else: | ||||
|             current_entry = self.entries[self.current_entry_index] | ||||
|             self.text_entry.text = current_entry.text | ||||
|             self.text_entry.read_only = False | ||||
|             self._original_content = current_entry.text | ||||
|             self.has_unsaved_changes = False | ||||
| 
 | ||||
|         self.call_after_refresh(self._finish_display_update) | ||||
| 
 | ||||
|     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 | ||||
|                 not getattr(self, '_updating_display', False) and hasattr(self, '_original_content')): | ||||
|             current_content = self.text_entry.text | ||||
|             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 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: | ||||
|             self.app.pop_screen() | ||||
|             self.notify("Returned to diary list") | ||||
|             return | ||||
| 
 | ||||
|         if self.has_unsaved_changes or (self.is_new_entry and self.text_entry.text.strip()): | ||||
|             self.notify("There are unsaved changes! Use Ctrl+S to save before leaving.") | ||||
|             return | ||||
| 
 | ||||
|         self.app.pop_screen() | ||||
|         self.notify("Returned to diary list") | ||||
| 
 | ||||
|     def action_next_entry(self) -> None: | ||||
|         """Goes to the next entry""" | ||||
|         self._save_current_state() | ||||
| 
 | ||||
|         if not self.entries: | ||||
|             if not self.is_new_entry: | ||||
|                 self.is_new_entry = True | ||||
|                 self._update_entry_display() | ||||
|                 self.notify("New entry created") | ||||
|             else: | ||||
|                 self.notify("Already in a new entry") | ||||
|             return | ||||
| 
 | ||||
|         if self.is_new_entry: | ||||
|             self.notify("Already at the last position (new entry)") | ||||
|         elif self.current_entry_index < len(self.entries) - 1: | ||||
|             self.current_entry_index += 1 | ||||
|             self._update_entry_display() | ||||
|             current_entry = self.entries[self.current_entry_index] | ||||
|             self.notify(f"Navigating to: {current_entry.title}") | ||||
|         else: | ||||
|             self.is_new_entry = True | ||||
|             self._update_entry_display() | ||||
|             self.notify("New entry created") | ||||
| 
 | ||||
|     def action_prev_entry(self) -> None: | ||||
|         """Goes to the previous entry""" | ||||
|         self._save_current_state() | ||||
| 
 | ||||
|         if not self.entries: | ||||
|             self.notify("No entries to navigate") | ||||
|             return | ||||
| 
 | ||||
|         if self.is_new_entry: | ||||
|             if self.entries: | ||||
|                 self.is_new_entry = False | ||||
|                 self.current_entry_index = len(self.entries) - 1 | ||||
|                 self._update_entry_display() | ||||
|                 current_entry = self.entries[self.current_entry_index] | ||||
|                 self.notify(f"Navigating to: {current_entry.title}") | ||||
|             else: | ||||
|                 self.notify("No previous entries") | ||||
|         elif self.current_entry_index > 0: | ||||
|             self.current_entry_index -= 1 | ||||
|             self._update_entry_display() | ||||
|             current_entry = self.entries[self.current_entry_index] | ||||
|             self.notify(f"Navigating to: {current_entry.title}") | ||||
|         else: | ||||
|             self.notify("Already at the first entry") | ||||
| 
 | ||||
|     def action_rename_entry(self) -> None: | ||||
|         """Opens a modal to rename the entry.""" | ||||
|         if not self.entries and not self.is_new_entry: | ||||
|             self.notify("No entry to rename", severity="warning") | ||||
|             return | ||||
| 
 | ||||
|         if self.is_new_entry: | ||||
|             current_name = self.new_entry_title | ||||
|         else: | ||||
|             current_entry = self.entries[self.current_entry_index] | ||||
|             current_name = current_entry.title | ||||
| 
 | ||||
|         self.app.push_screen( | ||||
|             RenameEntryModal(current_name=current_name), | ||||
|             self.handle_rename_result | ||||
|         ) | ||||
| 
 | ||||
|     def handle_rename_result(self, new_name: str | None) -> None: | ||||
|         """Callback that processes the rename modal result.""" | ||||
|         if new_name is None: | ||||
|             self.notify("Rename cancelled") | ||||
|             return | ||||
| 
 | ||||
|         if not new_name.strip(): | ||||
|             self.notify("Name cannot be empty", severity="error") | ||||
|             return | ||||
| 
 | ||||
|         if self.is_new_entry: | ||||
|             old_name = self.new_entry_title | ||||
|             self.new_entry_title = new_name | ||||
|             self.notify(f"New entry title changed to '{new_name}'") | ||||
|         else: | ||||
|             current_entry = self.entries[self.current_entry_index] | ||||
|             old_name = current_entry.title | ||||
|             current_entry.title = new_name | ||||
|             self.notify(f"Title changed from '{old_name}' to '{new_name}'") | ||||
| 
 | ||||
|         self.has_unsaved_changes = True | ||||
|         self._update_sub_header() | ||||
| 
 | ||||
|     def action_save(self) -> None: | ||||
|         """Saves the current entry""" | ||||
|         if self.is_new_entry: | ||||
|             content = self.text_entry.text.strip() | ||||
|             if not content: | ||||
|                 self.notify("Empty entry cannot be saved") | ||||
|                 return | ||||
| 
 | ||||
|             # Schedule async creation | ||||
|             self.call_later(self._async_create_entry, content) | ||||
|         else: | ||||
|             # Schedule async update | ||||
|             self.call_later(self._async_update_entry) | ||||
| 
 | ||||
|     async def _async_create_entry(self, content: str): | ||||
|         """Creates a new entry asynchronously""" | ||||
|         try: | ||||
|             service_manager = self.app.service_manager | ||||
|             entry_service = service_manager.get_entry_service() | ||||
| 
 | ||||
|             # Get current date | ||||
|             current_date = datetime.now().strftime("%d/%m/%Y") | ||||
| 
 | ||||
|             new_entry = entry_service.create( | ||||
|                 travel_diary_id=self.diary_id, | ||||
|                 title=self.new_entry_title, | ||||
|                 text=content, | ||||
|                 date=current_date | ||||
|             ) | ||||
| 
 | ||||
|             if new_entry: | ||||
|                 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 | ||||
|                         break | ||||
| 
 | ||||
|                 self.is_new_entry = False | ||||
|                 self.has_unsaved_changes = False | ||||
|                 self._original_content = new_entry.text | ||||
|                 self.new_entry_title = "New Entry" | ||||
|                 self.next_entry_id = max(entry.id for entry in self.entries) + 1 | ||||
| 
 | ||||
|                 self._update_entry_display() | ||||
|                 self.notify(f"New entry '{new_entry.title}' saved successfully!") | ||||
|             else: | ||||
|                 self.notify("Error creating entry") | ||||
| 
 | ||||
|         except Exception as e: | ||||
|             self.notify(f"Error creating entry: {str(e)}") | ||||
| 
 | ||||
|     async def _async_update_entry(self): | ||||
|         """Updates the current entry asynchronously""" | ||||
|         try: | ||||
|             if not self.entries: | ||||
|                 self.notify("No entry to update") | ||||
|                 return | ||||
| 
 | ||||
|             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, | ||||
|                 date=current_entry.date, | ||||
|                 travel_diary_id=current_entry.fk_travel_diary_id, | ||||
|                 id=current_entry.id | ||||
|             ) | ||||
| 
 | ||||
|             service_manager = self.app.service_manager | ||||
|             entry_service = service_manager.get_entry_service() | ||||
| 
 | ||||
|             result = entry_service.update(current_entry, updated_entry) | ||||
| 
 | ||||
|             if result: | ||||
|                 current_entry.text = updated_content | ||||
|                 self.has_unsaved_changes = False | ||||
|                 self._original_content = updated_content | ||||
|                 self._update_sub_header() | ||||
|                 self.notify(f"Entry '{current_entry.title}' saved successfully!") | ||||
|             else: | ||||
|                 self.notify("Error updating entry") | ||||
| 
 | ||||
|         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)  | ||||
|  | @ -0,0 +1,43 @@ | |||
| from textual.app import ComposeResult | ||||
| from textual.containers import Vertical, Horizontal | ||||
| from textual.screen import ModalScreen | ||||
| from textual.widgets import Label, Input, Button | ||||
| 
 | ||||
| class NewDiaryModal(ModalScreen[str]): | ||||
|     BINDINGS = [ | ||||
|         ("escape", "cancel", "Cancel"), | ||||
|     ] | ||||
|     def __init__(self): | ||||
|         super().__init__() | ||||
|         self.name_input = Input(id="NewDiaryModal-NameInput",classes="NewDiaryModal-NameInput") # This ID is fine, it's specific to the input | ||||
| 
 | ||||
|     def compose(self) -> ComposeResult: | ||||
| 
 | ||||
|         with Vertical(id="new_diary_dialog",classes="NewDiaryModal-Dialog"): | ||||
|             yield Label("Create a new diary", classes="NewDiaryModal-Title") | ||||
|             yield Label("Diary Name:") | ||||
|             yield self.name_input | ||||
|             with Horizontal(classes="NewDiaryModal-ButtonsContainer"): | ||||
|                 yield Button("Create", variant="primary", id="create_diary_button", | ||||
|                              classes="NewDiaryModal-CreateDiaryButton") | ||||
|                 yield Button("Cancel", variant="default", id="cancel_button", | ||||
|                              classes="NewDiaryModal-CancelButton") | ||||
| 
 | ||||
|     def on_mount(self): | ||||
|           self.name_input.focus() | ||||
| 
 | ||||
|     def on_button_pressed(self, event: Button.Pressed) -> None: | ||||
|         """Lida com os cliques dos botões.""" | ||||
|         if event.button.id == "create_diary_button": | ||||
|             diary_name = self.name_input.value.strip() | ||||
|             if diary_name: | ||||
|                 self.dismiss(diary_name) | ||||
|             else: | ||||
|                 self.notify("O nome do diário não pode estar vazio.", severity="warning") | ||||
|                 self.name_input.focus() | ||||
|         elif event.button.id == "cancel_button": | ||||
|             self.dismiss("") | ||||
| 
 | ||||
|     def action_cancel(self) -> None: | ||||
|         """Ação para cancelar a modal.""" | ||||
|         self.dismiss("") | ||||
|  | @ -0,0 +1,58 @@ | |||
| from textual.app import ComposeResult | ||||
| from textual.containers import Vertical, Horizontal | ||||
| from textual.screen import ModalScreen | ||||
| from textual.widgets import Label, Input, Button | ||||
| 
 | ||||
| 
 | ||||
| class RenameEntryModal(ModalScreen[str]): | ||||
|     """A modal screen to rename a diary entry.""" | ||||
| 
 | ||||
|     BINDINGS = [ | ||||
|         ("escape", "cancel", "Cancel"), | ||||
|     ] | ||||
| 
 | ||||
|     def __init__(self, current_name: str): | ||||
|         super().__init__() | ||||
|         self._current_name = current_name | ||||
|         self.name_input = Input( | ||||
|             value=self._current_name, | ||||
|             placeholder="Type the new name...", | ||||
|             id="rename_input", | ||||
|             classes="RenameEntryModal-name-input" | ||||
|         ) | ||||
| 
 | ||||
|     def compose(self) -> ComposeResult: | ||||
|         with Vertical(id="rename_entry_dialog", classes="RenameEntryModal-dialog"): | ||||
|             yield Label("Rename Entry", classes="dialog-title RenameEntryModal-title") | ||||
|             yield Label("New Entry Title:", classes="RenameEntryModal-label") | ||||
|             yield self.name_input | ||||
|             with Horizontal(classes="dialog-buttons RenameEntryModal-buttons"): | ||||
|                 yield Button("Save", variant="primary", id="save", classes="RenameEntryModal-save-button") | ||||
|                 yield Button("Cancel", variant="default", id="cancel", classes="RenameEntryModal-cancel-button") | ||||
| 
 | ||||
|     def on_mount(self) -> None: | ||||
|         """Focuses on the input when the screen is mounted.""" | ||||
|         self.name_input.focus() | ||||
|         self.name_input.cursor_position = len(self.name_input.value) | ||||
| 
 | ||||
|     def on_button_pressed(self, event: Button.Pressed) -> None: | ||||
|         """Handles button clicks.""" | ||||
|         if event.button.id == "save": | ||||
|             new_name = self.name_input.value.strip() | ||||
|             if new_name: | ||||
|                 self.dismiss(new_name)  # Returns the new name | ||||
|             else: | ||||
|                 self.dismiss(None)  # Considers empty name as cancellation | ||||
|         else: | ||||
|             self.dismiss(None)  # Returns None for cancellation | ||||
| 
 | ||||
|     def on_input_submitted(self, event: Input.Submitted) -> None: | ||||
|         """Allows saving by pressing Enter.""" | ||||
|         new_name = event.value.strip() | ||||
|         if new_name: | ||||
|             self.dismiss(new_name) | ||||
|         else: | ||||
|             self.dismiss(None) | ||||
|      | ||||
|     def action_cancel(self) -> None: | ||||
|         self.dismiss(None)  | ||||
|  | @ -0,0 +1,274 @@ | |||
| Screen { | ||||
|     layout: vertical; | ||||
|     background: $surface-darken-1; | ||||
|     align: center middle; | ||||
| } | ||||
| .DiaryListScreen-DiaryListContainer { | ||||
|     height: 1fr; | ||||
|     width: 100%; | ||||
|     layout: vertical; | ||||
|     align: center top; /* Alinha no topo para seguir o wireframe */ | ||||
|     padding: 2 4; /* Mais espaço nas laterais */ | ||||
|     background: $surface; /* Removendo aquamarine para ficar mais limpo */ | ||||
| } | ||||
| 
 | ||||
| /* A lista de diários - área principal como no wireframe */ | ||||
| .DiaryListScreen-DiaryListOptions { | ||||
|     border: round $primary-lighten-2; | ||||
|     background: $surface; | ||||
|     width: 75%; /* Largura generosa como no wireframe */ | ||||
|     height: 72%; /* Ocupa o espaço disponível */ | ||||
|     margin: 2 0; /* Margem vertical para separar dos outros elementos */ | ||||
| } | ||||
| 
 | ||||
| .DiaryListScreen-DiaryListOptions:focus { | ||||
|     border: round $primary; | ||||
| } | ||||
| 
 | ||||
| /* O contêiner dos botões - logo abaixo da lista */ | ||||
| .DiaryListScreen-ButtonsGrid { | ||||
|     layout: horizontal; | ||||
|     align: center middle; | ||||
|     width: 79%; /* Mesma largura da lista */ | ||||
|     height: 20%; /* Aumentando altura para caber o texto dos botões */ | ||||
| } | ||||
| 
 | ||||
| /* Botões individuais - garantindo espaço para o texto */ | ||||
| .DiaryListScreen-NewDiaryButton, | ||||
| .DiaryListScreen-EditDiaryButton, | ||||
| .DiaryListScreen-OpenDiaryButton { | ||||
|     margin: 0 1 1 0; /* Espaço entre os botões */ | ||||
|     height: 1fr; | ||||
|     width: 1fr; | ||||
|     border: round grey; | ||||
| } | ||||
| 
 | ||||
| /* As dicas - usando a classe correta */ | ||||
| .DiaryListScreen-DiaryListTips { | ||||
|     width: 100%; /* Largura total quando usando dock */ | ||||
|     height: auto; | ||||
|     text-style: italic; | ||||
|     text-align: center; /* Centraliza o texto dentro do elemento */ | ||||
|     color: $text-muted; | ||||
|     dock: bottom; /* Gruda no fundo da tela */ | ||||
|     margin: 0 1; | ||||
| } | ||||
| 
 | ||||
| /* Estilos genéricos que não mudaram */ | ||||
| .option-list--option-highlighted { | ||||
|     background: $primary-darken-2; | ||||
| } | ||||
| 
 | ||||
| .option-list--option-selected { | ||||
|     text-style: bold; | ||||
|     background: $primary; | ||||
| } | ||||
| 
 | ||||
| Header { | ||||
|     background: $primary; | ||||
|     color: $text; | ||||
| } | ||||
| 
 | ||||
| Footer { | ||||
|     background: $primary; | ||||
|     color: $text; | ||||
| } | ||||
| 
 | ||||
| AboutScreen { | ||||
|     align: center middle; | ||||
| } | ||||
| 
 | ||||
| .AboutScreen_AboutContainer { | ||||
|     align: center middle; | ||||
|     width: 85%; | ||||
|     height: 95%; | ||||
|     border: thick $primary; | ||||
|     background: $surface; | ||||
|     margin: 1; | ||||
| } | ||||
| 
 | ||||
| .AboutScreen_AboutContainer TextArea { | ||||
|     align: center middle; | ||||
|     width: 100%; | ||||
|     height: 55%; | ||||
|     margin: 2; | ||||
|     padding: 1; | ||||
| } | ||||
| 
 | ||||
| .AboutScreen_SubContainer { | ||||
|     align: center middle; | ||||
|     height: 45%; | ||||
|     padding: 1 1; | ||||
|     margin: 1 1; | ||||
| } | ||||
| 
 | ||||
| .AboutScreen_AboutTitle { | ||||
|     color: $accent; | ||||
|     text-style: bold; | ||||
|     text-align: center; | ||||
|     margin-bottom: 1; | ||||
| } | ||||
| 
 | ||||
| .AboutScreen_AboutVersion { | ||||
|     color: $warning; | ||||
|     text-style: italic; | ||||
|     text-align: center; | ||||
|     margin-bottom: 1; | ||||
| } | ||||
| 
 | ||||
| .AboutScreen_AboutContent { | ||||
|     text-align: center; | ||||
|     margin-bottom: 1; | ||||
| } | ||||
| 
 | ||||
| .AboutScreen_AboutContact, | ||||
| .AboutScreen_AboutAuthor { | ||||
|     color: $text-muted; | ||||
|     text-align: center; | ||||
|     margin-bottom: 1; | ||||
| } | ||||
| 
 | ||||
| .AboutScreen_AboutLicense { | ||||
|     height: 60%; | ||||
| } | ||||
| 
 | ||||
| Screen.-modal { | ||||
|     background: rgba(0, 0, 0, 0.7); /* Escurece o fundo da tela por baixo */ | ||||
|     align: center middle; /* Centraliza o conteúdo (sua modal) que está nesta tela modal */ | ||||
| } | ||||
| 
 | ||||
| .NewDiaryModal-Dialog { | ||||
|     width: 60%; /* Largura do diálogo */ | ||||
|     height: auto; /* Altura automática */ | ||||
|     background: $surface; /* Fundo do diálogo */ | ||||
|     border: thick $accent; /* Borda chamativa */ | ||||
|     padding: 2 4; /* Espaçamento interno */ | ||||
|     align: center middle; /* Centraliza conteúdo dentro do diálogo */ | ||||
|     layout: vertical; | ||||
| } | ||||
| 
 | ||||
| .NewDiaryModal-Dialog .NewDiaryModal-Title{ | ||||
|     text-align: center; | ||||
|     text-style: bold; | ||||
|     color: $primary; | ||||
|     margin-bottom: 1; | ||||
| } | ||||
| 
 | ||||
| .NewDiaryModal-NameInput{ | ||||
|     width: 1fr; /* Ocupa a largura disponível */ | ||||
|     margin-bottom: 2; | ||||
| } | ||||
| 
 | ||||
| .NewDiaryModal-ButtonsContainer{ | ||||
|     width: 1fr; | ||||
|     height: auto; | ||||
|     align: center middle; | ||||
|     padding-top: 1; | ||||
| } | ||||
| 
 | ||||
| .NewDiaryModal-ButtonsContainer Button{ | ||||
|     margin: 0 1; | ||||
|     width: 1fr; | ||||
| } | ||||
| 
 | ||||
| /* EditEntryScreen Styles */ | ||||
| .EditEntryScreen-sub-header { | ||||
|     layout: horizontal; | ||||
|     background: $primary-darken-1; | ||||
|     height: 1; | ||||
|     padding: 0 1; | ||||
|     align: center middle; | ||||
|     width: 100%; | ||||
| } | ||||
| 
 | ||||
| .EditEntryScreen-diary-info { | ||||
|     width: auto; | ||||
|     color: $text-muted; | ||||
|     margin-right: 2; | ||||
| } | ||||
| 
 | ||||
| .EditEntryScreen-entry-info { | ||||
|     width: auto; | ||||
|     color: $text; | ||||
|     margin-right: 1; | ||||
| } | ||||
| 
 | ||||
| .EditEntryScreen-spacer { | ||||
|     width: 1fr; | ||||
| } | ||||
| 
 | ||||
| .EditEntryScreen-status-indicator { | ||||
|     width: auto; | ||||
|     padding: 0 1; | ||||
|     content-align: center middle; | ||||
| } | ||||
| 
 | ||||
| .EditEntryScreen-status-indicator.saved { | ||||
|     background: #2E8B57; /* SeaGreen */ | ||||
|     color: $text; | ||||
| } | ||||
| 
 | ||||
| .EditEntryScreen-status-indicator.not-saved { | ||||
|     background: #B22222; /* FireBrick */ | ||||
|     color: $text; | ||||
| } | ||||
| 
 | ||||
| .EditEntryScreen-status-indicator.new { | ||||
|     background: #4682B4; /* SteelBlue */ | ||||
|     color: $text; | ||||
| } | ||||
| 
 | ||||
| .EditEntryScreen-status-indicator.read-only { | ||||
|     background: #696969; /* DimGray */ | ||||
|     color: $text-muted; | ||||
| } | ||||
| 
 | ||||
| .EditEntryScreen-main-container { | ||||
|     background: bisque; | ||||
|     margin: 0; | ||||
| } | ||||
| 
 | ||||
| .EditEntryScreen-text-entry { | ||||
|     width: 100%; | ||||
|     height: 1fr; | ||||
|     border: none; | ||||
| } | ||||
| 
 | ||||
| /* RenameEntryModal Styles */ | ||||
| .RenameEntryModal-dialog { | ||||
|     layout: vertical; | ||||
|     width: 60%; | ||||
|     height: auto; | ||||
|     background: $surface; | ||||
|     border: thick $accent; | ||||
|     padding: 2 4; | ||||
|     align: center middle; | ||||
| } | ||||
| 
 | ||||
| .RenameEntryModal-title { | ||||
|     text-align: center; | ||||
|     text-style: bold; | ||||
|     color: $accent; | ||||
|     margin-bottom: 1; | ||||
| } | ||||
| 
 | ||||
| .RenameEntryModal-label { | ||||
|     margin-bottom: 1; | ||||
| } | ||||
| 
 | ||||
| .RenameEntryModal-name-input { | ||||
|     width: 1fr; | ||||
|     margin-bottom: 2; | ||||
| } | ||||
| 
 | ||||
| .RenameEntryModal-buttons { | ||||
|     width: 1fr; | ||||
|     height: auto; | ||||
|     align: center middle; | ||||
|     padding-top: 1; | ||||
| } | ||||
| 
 | ||||
| .RenameEntryModal-buttons Button { | ||||
|     margin: 0 1; | ||||
|     width: 1fr; | ||||
| } | ||||
|  | @ -0,0 +1,58 @@ | |||
| from pathlib import Path | ||||
| from typing import Iterable | ||||
| 
 | ||||
| from textual.app import App, SystemCommand | ||||
| from textual.screen import Screen | ||||
| 
 | ||||
| 
 | ||||
| from pilgrim.service.servicemanager import ServiceManager | ||||
| from pilgrim.ui.screens.about_screen import AboutScreen | ||||
| from pilgrim.ui.screens.diary_list_screen import DiaryListScreen | ||||
| from pilgrim.ui.screens.edit_entry_screen import EditEntryScreen | ||||
| 
 | ||||
| CSS_FILE_PATH = Path(__file__).parent / "styles" / "pilgrim.css" | ||||
| 
 | ||||
| 
 | ||||
| class UIApp(App): | ||||
|     CSS_PATH = CSS_FILE_PATH | ||||
| 
 | ||||
|     def __init__(self,service_manager: ServiceManager, **kwargs): | ||||
|         super().__init__(**kwargs) | ||||
|         self.service_manager = service_manager | ||||
| 
 | ||||
| 
 | ||||
|     def on_mount(self) -> None: | ||||
|         """Chamado quando a app inicia. Carrega a tela principal.""" | ||||
|         self.push_screen(DiaryListScreen()) | ||||
| 
 | ||||
|     def get_system_commands(self, screen: Screen) -> Iterable[SystemCommand]: | ||||
|         """Return commands based on current screen.""" | ||||
| 
 | ||||
|         # Commands for DiaryListScreen | ||||
|         if isinstance(screen, DiaryListScreen): | ||||
|             yield SystemCommand( | ||||
|                 "About Pilgrim", | ||||
|                 "Open About Pilgrim", | ||||
|                 screen.action_about_cmd | ||||
|             ) | ||||
| 
 | ||||
|         elif isinstance(screen, AboutScreen): | ||||
|             yield SystemCommand( | ||||
|                 "Back to List", | ||||
|                 "Return to the diary list", | ||||
|                 screen.dismiss | ||||
|             ) | ||||
| 
 | ||||
|         elif isinstance(screen, EditEntryScreen): | ||||
|             yield SystemCommand( | ||||
|                 "Back to Diary List", | ||||
|                 "Return to the diary list", | ||||
|                 screen.action_back_to_list | ||||
|             ) | ||||
| 
 | ||||
|         # Always include quit command | ||||
|         yield SystemCommand( | ||||
|             "Quit Application", | ||||
|             "Exit Pilgrim", | ||||
|             self.action_quit | ||||
|         ) | ||||
		Loading…
	
		Reference in New Issue