adding file browsing

This commit is contained in:
Tim Bendt
2025-05-09 13:55:12 -06:00
parent e465825f16
commit 9ad483dca8
6 changed files with 509 additions and 155 deletions

View File

@@ -0,0 +1,187 @@
import os
import io
import asyncio
import tempfile
from typing import Optional
import aiohttp
import mammoth
from docx import Document
from textual.app import ComposeResult
from textual.binding import Binding
from textual.containers import Container, ScrollableContainer, Horizontal
from textual.screen import Screen
from textual.widgets import Label, Markdown, LoadingIndicator, Button, Footer
from textual.worker import Worker, get_current_worker
from textual import work
class DocumentViewerScreen(Screen):
"""Screen for viewing document content from OneDrive items."""
BINDINGS = [
Binding("escape", "close", "Close"),
Binding("q", "close", "Close"),
Binding("m", "toggle_mode", "Toggle Mode"),
]
def __init__(self, item_id: str, item_name: str, access_token: str, drive_id: str):
"""Initialize the document viewer screen.
Args:
item_id: The ID of the item to view.
item_name: The name of the item to display.
access_token: The access token for API requests.
"""
super().__init__()
self.item_id = item_id
self.drive_id = drive_id
self.item_name = item_name
self.access_token = access_token
self.document_content = ""
self.plain_text_content = ""
self.is_markdown_mode = False
self.content_type = None
self.raw_content = None
def compose(self) -> ComposeResult:
"""Compose the document viewer screen."""
yield Container(
Horizontal(
Label(f"Viewing: {self.item_name}", id="document_title"),
Container(
Button("Close", id="close_button"),
Button("Toggle Mode", id="toggle_mode_button"),
id="button_container"
),
id="top_container"
),
ScrollableContainer(
Markdown("", id="markdown_content"),
Label("", id="plaintext_content", classes="hidden", markup=False),
id="content_container",
),
id="document_viewer"
)
yield Footer()
def on_mount(self) -> None:
"""Handle screen mount event."""
self.download_document()
def on_button_pressed(self, event: Button.Pressed) -> None:
"""Handle button press events."""
if event.button.id == "close_button":
self.dismiss()
elif event.button.id == "toggle_mode_button":
self.action_toggle_mode()
@work
async def download_document(self) -> None:
"""Download the document content."""
try:
url = f"https://graph.microsoft.com/v1.0/drives/{self.drive_id}/items/{self.item_id}/content"
headers = {"Authorization": f"Bearer {self.access_token}"}
# Show loading indicator
self.query_one("#content_container").loading = True
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers) as response:
if response.status != 200:
error_text = await response.text()
self.notify(f"Failed to download document: {error_text}", severity="error")
return
self.content_type = response.headers.get("content-type", "")
self.raw_content = await response.read()
# Process the content based on content type
self.process_content()
except Exception as e:
self.notify(f"Error downloading document: {str(e)}", severity="error")
finally:
# Hide loading indicator
self.query_one("#content_container").loading = False
@work
async def process_content(self) -> None:
"""Process the downloaded content based on its type."""
if not self.raw_content:
self.notify("No content to display", severity="warning")
return
try:
# Check for Office document types
if self.content_type == "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
# Process as DOCX
self.process_docx()
elif self.content_type.startswith("text/"):
# Process as plain text
text_content = self.raw_content.decode("utf-8", errors="replace")
self.document_content = text_content
self.update_content_display()
elif self.content_type.startswith("image/"):
# For images, just display a message
self.document_content = f"*Image file: {self.item_name}*\n\nUse the 'Open URL' command to view this image in your browser."
self.update_content_display()
else:
# For other types, display a generic message
self.document_content = f"*File: {self.item_name}*\n\nContent type: {self.content_type}\n\nThis file type cannot be displayed in the viewer. Use the 'Open URL' command to view this file in your browser."
self.update_content_display()
except Exception as e:
self.notify(f"Error processing content: {str(e)}", severity="error")
@work
async def process_docx(self) -> None:
"""Process DOCX content and convert to Markdown and plain text."""
try:
# Save the DOCX content to a temporary file
with tempfile.NamedTemporaryFile(suffix=".docx", delete=False) as temp_file:
temp_file.write(self.raw_content)
temp_path = temp_file.name
# Convert DOCX to Markdown using mammoth
with open(temp_path, "rb") as docx_file:
result = mammoth.convert_to_markdown(docx_file)
markdown_text = result.value
# Read the document structure with python-docx for plain text
doc = Document(temp_path)
self.plain_text_content = "\n\n".join([para.text for para in doc.paragraphs if para.text])
self.document_content = markdown_text
# Clean up temporary file
os.unlink(temp_path)
# Store both versions
self.update_content_display()
except Exception as e:
self.notify(f"Error processing DOCX: {str(e)}", severity="error")
def update_content_display(self) -> None:
"""Update the content display with the processed document content."""
markdown_widget = self.query_one("#markdown_content", Markdown)
plaintext_widget = self.query_one("#plaintext_content", Label)
if self.is_markdown_mode:
markdown_widget.update(self.document_content)
markdown_widget.remove_class("hidden")
plaintext_widget.add_class("hidden")
else:
plaintext_widget.update(self.plain_text_content)
plaintext_widget.remove_class("hidden")
markdown_widget.add_class("hidden")
async def action_toggle_mode(self) -> None:
"""Toggle between Markdown and plaintext display modes."""
self.is_markdown_mode = not self.is_markdown_mode
self.update_content_display()
mode_name = "Markdown" if self.is_markdown_mode else "Plain Text"
self.notify(f"Switched to {mode_name} mode")
async def action_close(self) -> None:
"""Close the document viewer screen."""
self.dismiss()

View File

@@ -1 +1,6 @@
# Initialize the screens subpackage
# Initialize the screens package
from maildir_gtd.screens.CreateTask import CreateTaskScreen
from maildir_gtd.screens.OpenMessage import OpenMessageScreen
from maildir_gtd.screens.DocumentViewer import DocumentViewerScreen
__all__ = ["CreateTaskScreen", "OpenMessageScreen", "DocumentViewerScreen"]