wip
This commit is contained in:
@@ -216,6 +216,8 @@ async def _sync_outlook_data(dry_run, vdir, icsfile, org, days_back, days_forwar
|
|||||||
task_archive = progress.add_task("[yellow]Archiving mail...", total=0)
|
task_archive = progress.add_task("[yellow]Archiving mail...", total=0)
|
||||||
task_delete = progress.add_task("[red]Deleting mail...", total=0)
|
task_delete = progress.add_task("[red]Deleting mail...", total=0)
|
||||||
|
|
||||||
|
# Stage 1: Synchronize local changes (read, archive, delete) to the server
|
||||||
|
progress.console.print("[bold cyan]Step 1: Syncing local changes to server...[/bold cyan]")
|
||||||
await asyncio.gather(
|
await asyncio.gather(
|
||||||
synchronize_maildir_async(
|
synchronize_maildir_async(
|
||||||
maildir_path, headers, progress, task_read, dry_run
|
maildir_path, headers, progress, task_read, dry_run
|
||||||
@@ -224,6 +226,12 @@ async def _sync_outlook_data(dry_run, vdir, icsfile, org, days_back, days_forwar
|
|||||||
progress, task_archive, dry_run),
|
progress, task_archive, dry_run),
|
||||||
delete_mail_async(maildir_path, headers,
|
delete_mail_async(maildir_path, headers,
|
||||||
progress, task_delete, dry_run),
|
progress, task_delete, dry_run),
|
||||||
|
)
|
||||||
|
progress.console.print("[bold green]Step 1: Local changes synced.[/bold green]")
|
||||||
|
|
||||||
|
# Stage 2: Fetch new data from the server
|
||||||
|
progress.console.print("\n[bold cyan]Step 2: Fetching new data from server...[/bold cyan]")
|
||||||
|
await asyncio.gather(
|
||||||
fetch_mail_async(
|
fetch_mail_async(
|
||||||
maildir_path,
|
maildir_path,
|
||||||
attachments_dir,
|
attachments_dir,
|
||||||
@@ -235,6 +243,7 @@ async def _sync_outlook_data(dry_run, vdir, icsfile, org, days_back, days_forwar
|
|||||||
),
|
),
|
||||||
fetch_calendar_async(headers, progress, task_calendar, dry_run, vdir, icsfile, org, days_back, days_forward, continue_iteration),
|
fetch_calendar_async(headers, progress, task_calendar, dry_run, vdir, icsfile, org, days_back, days_forward, continue_iteration),
|
||||||
)
|
)
|
||||||
|
progress.console.print("[bold green]Step 2: New data fetched.[/bold green]")
|
||||||
click.echo("Sync complete.")
|
click.echo("Sync complete.")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ from .widgets.EnvelopeHeader import EnvelopeHeader
|
|||||||
from .actions.task import action_create_task
|
from .actions.task import action_create_task
|
||||||
from .actions.open import action_open
|
from .actions.open import action_open
|
||||||
from .actions.delete import delete_current
|
from .actions.delete import delete_current
|
||||||
from .actions.archive import archive_current
|
|
||||||
from src.services.taskwarrior import client as taskwarrior_client
|
from src.services.taskwarrior import client as taskwarrior_client
|
||||||
from src.services.himalaya import client as himalaya_client
|
from src.services.himalaya import client as himalaya_client
|
||||||
from textual.containers import ScrollableContainer, Vertical, Horizontal
|
from textual.containers import ScrollableContainer, Vertical, Horizontal
|
||||||
@@ -69,6 +68,7 @@ class EmailViewerApp(App):
|
|||||||
status_title = reactive("Message View")
|
status_title = reactive("Message View")
|
||||||
sort_order_ascending: Reactive[bool] = reactive(True)
|
sort_order_ascending: Reactive[bool] = reactive(True)
|
||||||
selected_messages: Reactive[set[int]] = reactive(set())
|
selected_messages: Reactive[set[int]] = reactive(set())
|
||||||
|
main_content_visible: Reactive[bool] = reactive(True)
|
||||||
|
|
||||||
def get_system_commands(self, screen: Screen) -> Iterable[SystemCommand]:
|
def get_system_commands(self, screen: Screen) -> Iterable[SystemCommand]:
|
||||||
yield from super().get_system_commands(screen)
|
yield from super().get_system_commands(screen)
|
||||||
@@ -110,7 +110,7 @@ class EmailViewerApp(App):
|
|||||||
Binding("2", "focus_2", "Focus Folders Panel"),
|
Binding("2", "focus_2", "Focus Folders Panel"),
|
||||||
Binding("3", "focus_3", "Focus Envelopes Panel"),
|
Binding("3", "focus_3", "Focus Envelopes Panel"),
|
||||||
Binding("4", "focus_4", "Focus Main Content"),
|
Binding("4", "focus_4", "Focus Main Content"),
|
||||||
Binding("m", "toggle_mode", "Toggle Content Mode"),
|
Binding("w", "toggle_main_content", "Toggle Message View Window"),
|
||||||
]
|
]
|
||||||
|
|
||||||
BINDINGS.extend(
|
BINDINGS.extend(
|
||||||
@@ -215,35 +215,35 @@ class EmailViewerApp(App):
|
|||||||
)
|
)
|
||||||
if new_message_id == old_message_id:
|
if new_message_id == old_message_id:
|
||||||
return
|
return
|
||||||
self.msg_worker.cancel() if self.msg_worker else None
|
|
||||||
logging.info(f"new_message_id: {new_message_id}, type: {
|
|
||||||
type(new_message_id)}")
|
|
||||||
|
|
||||||
|
# If the main content view is not visible, don't load the message
|
||||||
|
if not self.main_content_visible:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Cancel any existing message loading worker
|
||||||
|
if self.msg_worker:
|
||||||
|
self.msg_worker.cancel()
|
||||||
|
|
||||||
|
# Start a new worker to load the message content
|
||||||
|
self.msg_worker = self.load_message_content(new_message_id)
|
||||||
|
|
||||||
|
@work(exclusive=True)
|
||||||
|
async def load_message_content(self, message_id: int) -> None:
|
||||||
|
"""Worker to load message content asynchronously."""
|
||||||
content_container = self.query_one(ContentContainer)
|
content_container = self.query_one(ContentContainer)
|
||||||
content_container.display_content(new_message_id)
|
content_container.display_content(message_id)
|
||||||
|
|
||||||
metadata = self.message_store.get_metadata(new_message_id)
|
metadata = self.message_store.get_metadata(message_id)
|
||||||
if metadata:
|
if metadata:
|
||||||
# Pass the complete date string with timezone information
|
|
||||||
message_date = metadata["date"]
|
message_date = metadata["date"]
|
||||||
|
|
||||||
if self.current_message_index != metadata["index"]:
|
if self.current_message_index != metadata["index"]:
|
||||||
self.current_message_index = metadata["index"]
|
self.current_message_index = metadata["index"]
|
||||||
|
|
||||||
# content_container.update_header(
|
|
||||||
# subject=metadata.get("subject", "").strip(),
|
|
||||||
# from_=metadata["from"].get("addr", ""),
|
|
||||||
# to=metadata["to"].get("addr", ""),
|
|
||||||
# date=message_date,
|
|
||||||
# cc=metadata["cc"].get("addr", "") if "cc" in metadata else "",
|
|
||||||
# )
|
|
||||||
|
|
||||||
list_view = self.query_one("#envelopes_list", ListView)
|
list_view = self.query_one("#envelopes_list", ListView)
|
||||||
if list_view.index != metadata["index"]:
|
if list_view.index != metadata["index"]:
|
||||||
list_view.index = metadata["index"]
|
list_view.index = metadata["index"]
|
||||||
else:
|
else:
|
||||||
logging.warning(
|
logging.warning(f"Message ID {message_id} not found in metadata.")
|
||||||
f"Message ID {new_message_id} not found in metadata.")
|
|
||||||
|
|
||||||
def on_list_view_selected(self, event: ListView.Selected) -> None:
|
def on_list_view_selected(self, event: ListView.Selected) -> None:
|
||||||
"""Called when an item in the list view is selected."""
|
"""Called when an item in the list view is selected."""
|
||||||
@@ -286,9 +286,11 @@ class EmailViewerApp(App):
|
|||||||
# Use the Himalaya client to fetch envelopes
|
# Use the Himalaya client to fetch envelopes
|
||||||
envelopes, success = await himalaya_client.list_envelopes()
|
envelopes, success = await himalaya_client.list_envelopes()
|
||||||
|
|
||||||
if success and envelopes:
|
if success:
|
||||||
self.reload_needed = False
|
self.reload_needed = False
|
||||||
self.message_store.load(envelopes, self.sort_order_ascending)
|
# Ensure envelopes is a list, even if it's None from the client
|
||||||
|
envelopes_list = envelopes if envelopes is not None else []
|
||||||
|
self.message_store.load(envelopes_list, self.sort_order_ascending)
|
||||||
self.total_messages = self.message_store.total_messages
|
self.total_messages = self.message_store.total_messages
|
||||||
|
|
||||||
# Use the centralized refresh method to update the ListView
|
# Use the centralized refresh method to update the ListView
|
||||||
@@ -530,20 +532,72 @@ class EmailViewerApp(App):
|
|||||||
await worker.wait()
|
await worker.wait()
|
||||||
|
|
||||||
async def action_archive(self) -> None:
|
async def action_archive(self) -> None:
|
||||||
"""Archive the current message and update UI consistently."""
|
"""Archive the current or selected messages and update UI consistently."""
|
||||||
|
next_id_to_select = None
|
||||||
|
|
||||||
if self.selected_messages:
|
if self.selected_messages:
|
||||||
message_ids = [str(msg_id) for msg_id in self.selected_messages]
|
# --- Multi-message archive ---
|
||||||
_, success = await himalaya_client.archive_messages(message_ids)
|
message_ids_to_archive = list(self.selected_messages)
|
||||||
|
|
||||||
|
if message_ids_to_archive:
|
||||||
|
highest_archived_id = max(message_ids_to_archive)
|
||||||
|
metadata = self.message_store.get_metadata(highest_archived_id)
|
||||||
|
if metadata:
|
||||||
|
next_id, _ = self.message_store.find_next_valid_id(metadata["index"])
|
||||||
|
if next_id is None:
|
||||||
|
next_id, _ = self.message_store.find_prev_valid_id(
|
||||||
|
metadata["index"]
|
||||||
|
)
|
||||||
|
next_id_to_select = next_id
|
||||||
|
|
||||||
|
message, success = await himalaya_client.archive_messages(
|
||||||
|
[str(mid) for mid in message_ids_to_archive]
|
||||||
|
)
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
self.show_status(f"{len(message_ids)} messages archived.")
|
self.show_status(message)
|
||||||
self.selected_messages.clear()
|
self.selected_messages.clear()
|
||||||
self.fetch_envelopes()
|
|
||||||
else:
|
else:
|
||||||
self.show_status("Failed to archive messages.", "error")
|
self.show_status(f"Failed to archive messages: {message}", "error")
|
||||||
|
return
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Call the archive_current function which uses our Himalaya client module
|
# --- Single message archive ---
|
||||||
worker = archive_current(self)
|
if not self.current_message_id:
|
||||||
await worker.wait()
|
self.show_status("No message selected to archive.", "error")
|
||||||
|
return
|
||||||
|
|
||||||
|
current_id = self.current_message_id
|
||||||
|
current_idx = self.current_message_index
|
||||||
|
|
||||||
|
next_id, _ = self.message_store.find_next_valid_id(current_idx)
|
||||||
|
if next_id is None:
|
||||||
|
next_id, _ = self.message_store.find_prev_valid_id(current_idx)
|
||||||
|
next_id_to_select = next_id
|
||||||
|
|
||||||
|
message, success = await himalaya_client.archive_message(current_id)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
self.show_status(message)
|
||||||
|
else:
|
||||||
|
self.show_status(
|
||||||
|
f"Failed to archive message {current_id}: {message}", "error"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Refresh the envelope list
|
||||||
|
worker = self.fetch_envelopes()
|
||||||
|
await worker.wait()
|
||||||
|
|
||||||
|
# After refresh, select the next message
|
||||||
|
if next_id_to_select:
|
||||||
|
new_metadata = self.message_store.get_metadata(next_id_to_select)
|
||||||
|
if new_metadata:
|
||||||
|
self.current_message_id = next_id_to_select
|
||||||
|
else:
|
||||||
|
self.action_oldest()
|
||||||
|
else:
|
||||||
|
self.action_oldest()
|
||||||
|
|
||||||
def action_open(self) -> None:
|
def action_open(self) -> None:
|
||||||
action_open(self)
|
action_open(self)
|
||||||
@@ -567,6 +621,22 @@ class EmailViewerApp(App):
|
|||||||
"""Scroll the main content up by a page."""
|
"""Scroll the main content up by a page."""
|
||||||
self.query_one("#main_content").scroll_page_up()
|
self.query_one("#main_content").scroll_page_up()
|
||||||
|
|
||||||
|
def action_toggle_main_content(self) -> None:
|
||||||
|
"""Toggle the visibility of the main content pane."""
|
||||||
|
self.main_content_visible = not self.main_content_visible
|
||||||
|
|
||||||
|
def watch_main_content_visible(self, visible: bool) -> None:
|
||||||
|
"""Called when main_content_visible changes."""
|
||||||
|
main_content = self.query_one("#main_content")
|
||||||
|
accounts_list = self.query_one("#accounts_list")
|
||||||
|
folders_list = self.query_one("#folders_list")
|
||||||
|
|
||||||
|
main_content.display = visible
|
||||||
|
accounts_list.display = visible
|
||||||
|
folders_list.display = visible
|
||||||
|
|
||||||
|
self.query_one("#envelopes_list").focus()
|
||||||
|
|
||||||
def action_quit(self) -> None:
|
def action_quit(self) -> None:
|
||||||
self.exit()
|
self.exit()
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from markitdown import MarkItDown
|
from markitdown import MarkItDown
|
||||||
from textual import work
|
from textual import work
|
||||||
|
from textual.binding import Binding
|
||||||
from textual.containers import Vertical, ScrollableContainer
|
from textual.containers import Vertical, ScrollableContainer
|
||||||
from textual.widgets import Static, Markdown, Label
|
from textual.widgets import Static, Markdown, Label
|
||||||
from src.services.himalaya import client as himalaya_client
|
from src.services.himalaya import client as himalaya_client
|
||||||
@@ -57,6 +58,9 @@ class EnvelopeHeader(Vertical):
|
|||||||
|
|
||||||
class ContentContainer(ScrollableContainer):
|
class ContentContainer(ScrollableContainer):
|
||||||
can_focus = True
|
can_focus = True
|
||||||
|
BINDINGS = [
|
||||||
|
Binding("m", "toggle_mode", "Toggle View Mode")
|
||||||
|
]
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
@@ -79,7 +83,7 @@ class ContentContainer(ScrollableContainer):
|
|||||||
self.content.styles.display = "none"
|
self.content.styles.display = "none"
|
||||||
self.html_content.styles.display = "block"
|
self.html_content.styles.display = "block"
|
||||||
|
|
||||||
async def toggle_mode(self):
|
async def action_toggle_mode(self):
|
||||||
"""Toggle between plaintext and HTML viewing modes."""
|
"""Toggle between plaintext and HTML viewing modes."""
|
||||||
if self.current_mode == "html":
|
if self.current_mode == "html":
|
||||||
self.current_mode = "text"
|
self.current_mode = "text"
|
||||||
@@ -91,6 +95,7 @@ class ContentContainer(ScrollableContainer):
|
|||||||
self.html_content.styles.display = "block"
|
self.html_content.styles.display = "block"
|
||||||
# self.action_notify(f"switched to mode {self.current_mode}")
|
# self.action_notify(f"switched to mode {self.current_mode}")
|
||||||
# Reload the content if we have a message ID
|
# Reload the content if we have a message ID
|
||||||
|
self.border_sibtitle = self.current_mode;
|
||||||
if self.current_message_id:
|
if self.current_message_id:
|
||||||
self.display_content(self.current_message_id)
|
self.display_content(self.current_message_id)
|
||||||
|
|
||||||
@@ -119,6 +124,12 @@ class ContentContainer(ScrollableContainer):
|
|||||||
|
|
||||||
self.current_message_id = message_id
|
self.current_message_id = message_id
|
||||||
|
|
||||||
|
# Immediately show a loading message
|
||||||
|
if self.current_mode == "text":
|
||||||
|
self.content.update("Loading...")
|
||||||
|
else:
|
||||||
|
self.html_content.update("Loading...")
|
||||||
|
|
||||||
# Cancel any existing content fetch operations
|
# Cancel any existing content fetch operations
|
||||||
if self.content_worker:
|
if self.content_worker:
|
||||||
self.content_worker.cancel()
|
self.content_worker.cancel()
|
||||||
|
|||||||
Reference in New Issue
Block a user