clean up UI on drive viewer

This commit is contained in:
Tim Bendt
2025-05-12 14:29:04 -06:00
parent 64146abb4e
commit d75f16c25d
5 changed files with 254 additions and 50 deletions

View File

@@ -30,6 +30,9 @@ from textual.worker import Worker, get_current_worker
from textual import work from textual import work
from textual.widgets.option_list import Option from textual.widgets.option_list import Option
# Import file icons utility - note the updated import
from utils.file_icons import get_file_icon
# Import our DocumentViewerScreen # Import our DocumentViewerScreen
sys.path.append(os.path.join(os.path.dirname(__file__), "maildir_gtd")) sys.path.append(os.path.join(os.path.dirname(__file__), "maildir_gtd"))
from maildir_gtd.screens.DocumentViewer import DocumentViewerScreen from maildir_gtd.screens.DocumentViewer import DocumentViewerScreen
@@ -96,26 +99,24 @@ class OneDriveTUI(App):
yield Header(show_clock=True) yield Header(show_clock=True)
with Container(id="main_container"): with Container(id="main_container"):
yield LoadingIndicator(id="loading") with Horizontal(id="top_bar"):
yield Label("Authenticating with Microsoft Graph API...", id="status_label") yield Button("\uf148 Up", id="back_button", classes="hide")
yield Label("Authenticating with Microsoft Graph API...", id="status_label")
yield LoadingIndicator(id="loading")
yield OptionList(
Option("Following", id="following"),
Option("Root", id="root"),
id="view_options"
)
with Container(id="auth_container"): with Container(id="auth_container"):
yield Label("", id="auth_message") yield Label("", id="auth_message")
yield Button("Login", id="login_button", variant="primary") yield Button("Login", id="login_button", variant="primary")
with Container(id="content_container", classes="hide"): with Container(id="content_container", classes="hide"):
with Horizontal(): with Vertical(id="items_container"):
with Vertical(id="navigation_container"): yield DataTable(id="items_table")
yield Label("No items found", id="no_items_label", classes="hide")
yield OptionList(
Option("Following", id="following"),
Option("Root", id="root"),
id="view_options"
)
with Vertical(id="items_container"):
yield DataTable(id="items_table")
yield Label("No items found", id="no_items_label", classes="hide")
yield Footer() yield Footer()
@@ -126,7 +127,7 @@ class OneDriveTUI(App):
# Initialize the table # Initialize the table
table = self.query_one("#items_table") table = self.query_one("#items_table")
table.cursor_type = "row" table.cursor_type = "row"
table.add_columns("Type", "Name", "Last Modified", "Size", "Web URL") table.add_columns("", "Name", "Last Modified", "Size", "Web URL")
# Load cached token if available # Load cached token if available
if os.path.exists(self.cache_file): if os.path.exists(self.cache_file):
@@ -183,6 +184,8 @@ class OneDriveTUI(App):
async def on_button_pressed(self, event: Button.Pressed) -> None: async def on_button_pressed(self, event: Button.Pressed) -> None:
"""Handle button presses.""" """Handle button presses."""
if event.button.id == "back_button":
self.action_navigate_back()
if event.button.id == "login_button": if event.button.id == "login_button":
self.initiate_device_flow() self.initiate_device_flow()
@@ -275,7 +278,7 @@ class OneDriveTUI(App):
elif selected_option == "root": elif selected_option == "root":
self.current_view = "Root" self.current_view = "Root"
if self.selected_drive_id: if self.selected_drive_id:
self.load_root_items() self.load_drive_folder_items()
async def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None: async def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
"""Handle row selection in the items table.""" """Handle row selection in the items table."""
@@ -287,6 +290,7 @@ class OneDriveTUI(App):
def open_item(self, selected_id: str): def open_item(self, selected_id: str):
if selected_id: if selected_id:
self.folder_history.append(FolderHistoryEntry(self.current_folder_id, self.current_folder_name, self.selected_drive_id))
# Get an item from current items by ID string # Get an item from current items by ID string
selected_row = self.current_items[selected_id] selected_row = self.current_items[selected_id]
@@ -294,19 +298,19 @@ class OneDriveTUI(App):
# Check if it's a folder # Check if it's a folder
is_folder = bool(selected_row.get("folder")) is_folder = bool(selected_row.get("folder"))
if is_folder: if is_folder:
self.notify(f"Selected folder: {item_name}") self.notify(f"Selected folder: {item_name}", timeout=1)
# Load items in the folder # Load items in the folder
self.query_one("#back_button").remove_class("hide")
self.query_one("#status_label").update(f"Loading items in folder: {item_name}") self.query_one("#status_label").update(f"Loading items in folder: {item_name}")
self.load_root_items(folder_id=selected_id, drive_id=selected_row.get("parentReference", {}).get("driveId", self.selected_drive_id)) self.load_drive_folder_items(folder_id=selected_id, drive_id=selected_row.get("parentReference", {}).get("driveId", self.selected_drive_id))
else: else:
self.notify(f"Selected file: {item_name}") self.notify(f"Selected file: {item_name}")
self.action_view_document() self.action_view_document()
@work @work
async def load_root_items(self, folder_id: str = "", drive_id: str = "", track_history: bool = True): async def load_drive_folder_items(self, folder_id: str = "", drive_id: str = "", track_history: bool = True):
"""Load root items from the selected drive.""" """Load root items from the selected drive."""
if not self.access_token or not self.selected_drive_id: if not self.access_token or not self.selected_drive_id:
return return
@@ -315,10 +319,10 @@ class OneDriveTUI(App):
headers = {"Authorization": f"Bearer {self.access_token}"} headers = {"Authorization": f"Bearer {self.access_token}"}
url = f"https://graph.microsoft.com/v1.0/me/drive/root/children" url = f"https://graph.microsoft.com/v1.0/me/drive/root/children"
if folder_id and drive_id: if folder_id and drive_id and folder_id != "root":
url = f"https://graph.microsoft.com/v1.0/drives/{drive_id}/items/{folder_id}/children" url = f"https://graph.microsoft.com/v1.0/drives/{drive_id}/items/{folder_id}/children"
if track_history: # if track_history:
self.folder_history.append(FolderHistoryEntry(folder_id, self.current_folder_name, drive_id)) # self.folder_history.append(FolderHistoryEntry(folder_id, self.current_folder_name, drive_id))
self.selected_drive_id = drive_id self.selected_drive_id = drive_id
self.current_folder_id = folder_id self.current_folder_id = folder_id
self.current_folder_name = self.current_items[folder_id].get("name", "Unknown") self.current_folder_name = self.current_items[folder_id].get("name", "Unknown")
@@ -338,7 +342,13 @@ class OneDriveTUI(App):
except Exception as e: except Exception as e:
self.notify(f"Error loading root items: {str(e)}", severity="error") self.notify(f"Error loading root items: {str(e)}", severity="error")
self.query_one("#status_label").update("Ready") #update the status label with breadcrumbs from the folder_history
if self.folder_history:
breadcrumbs = " / \uf07b ".join([entry.folder_name for entry in self.folder_history])
self.query_one("#status_label").update(f"\uf07b {breadcrumbs} / \uf07b {self.current_folder_name}")
else:
self.query_one("#status_label").update(f" \uf07b {self.current_folder_name}")
@work @work
async def load_followed_items(self): async def load_followed_items(self):
@@ -384,9 +394,8 @@ class OneDriveTUI(App):
name = item.get("name", "Unknown") name = item.get("name", "Unknown")
is_folder = bool(item.get("folder")) is_folder = bool(item.get("folder"))
# Add folder icon if it's a folder and we're in root view # Get icon for the file type
item_type = get_file_icon(name, is_folder)
item_type = "📁" if is_folder else "📄"
# Format the last modified date # Format the last modified date
last_modified = item.get("lastModifiedDateTime", "") last_modified = item.get("lastModifiedDateTime", "")
@@ -413,9 +422,21 @@ class OneDriveTUI(App):
web_url = item.get("webUrl", "") web_url = item.get("webUrl", "")
item_id = item.get("id") item_id = item.get("id")
item_drive_id = item.get("parentReference", {}).get("driveId", self.selected_drive_id)
row_key = table.add_row(item_type, name, last_modified, size_str, web_url, key=item.get("id")) # Limit filename length to 160 characters
# add item to to the list of current items keyed by row_key so we can look up all information later display_name = name[:50] + '...' if len(name) > 50 else name
# Add row to table with the appropriate icon class for styling
row_key = table.add_row(
item_type,
display_name,
last_modified,
size_str,
web_url,
key=item.get("id")
)
# Add item to the list of current items keyed by row_key so we can look up all information later
self.current_items[row_key] = item self.current_items[row_key] = item
async def action_next_view(self) -> None: async def action_next_view(self) -> None:
@@ -424,7 +445,7 @@ class OneDriveTUI(App):
if self.current_view == "Following": if self.current_view == "Following":
option_list.highlighted = 1 # Switch to Root option_list.highlighted = 1 # Switch to Root
self.current_view = "Root" self.current_view = "Root"
self.load_root_items() self.load_drive_folder_items()
else: else:
option_list.highlighted = 0 # Switch to Following option_list.highlighted = 0 # Switch to Following
self.current_view = "Following" self.current_view = "Following"
@@ -438,7 +459,7 @@ class OneDriveTUI(App):
if self.current_view == "Following": if self.current_view == "Following":
self.load_followed_items() self.load_followed_items()
elif self.current_view == "Root": elif self.current_view == "Root":
self.load_root_items() self.load_drive_folder_items()
self.notify("Refreshed items") self.notify("Refreshed items")
async def action_toggle_follow(self) -> None: async def action_toggle_follow(self) -> None:
@@ -479,13 +500,15 @@ class OneDriveTUI(App):
"""Quit the application.""" """Quit the application."""
self.exit() self.exit()
async def action_navigate_back(self) -> None: def action_navigate_back(self) -> None:
"""Navigate back to the previous folder.""" """Navigate back to the previous folder."""
if self.folder_history: if self.folder_history:
previous_entry = self.folder_history.pop() previous_entry = self.folder_history.pop()
self.current_folder_id = previous_entry.folder_id self.current_folder_id = previous_entry.folder_id
self.current_folder_name = previous_entry.folder_name self.current_folder_name = previous_entry.folder_name
self.load_root_items(folder_id=previous_entry.folder_id, drive_id=previous_entry.parent_id, track_history=False) if len(self.folder_history) <= 0:
self.query_one("#back_button").add_class("hide")
self.load_drive_folder_items(folder_id=previous_entry.folder_id, drive_id=previous_entry.parent_id, track_history=False)
else: else:
self.notify("No previous folder to navigate back to") self.notify("No previous folder to navigate back to")

View File

@@ -42,18 +42,26 @@
} }
/* Status and loading elements */ /* Status and loading elements */
#status_label {
#top_bar {
height: 4;
#status_label {
text-align: center; text-align: center;
color: $accent; color: $accent;
padding:1; padding:1;
width: 2fr
}
#view_options {
border: round $secondary;
width: 1fr;
min-width: 40;
}
} }
#view_options {
border: round $secondary;
}
#loading_container { #loading_container {
height: 3; height: 3;
width: 100%; width: 100%;
@@ -96,11 +104,42 @@
height: 100%; height: 100%;
} }
#items_table { #items_table {
width: 100%; width: 100%;
height: auto; height: auto;
} }
/* File icon styling in the data table */
.datatable--cell:first-child {
color: $accent;
text-align: center;
padding-right: 1;
min-width: 3;
}
/* Custom icon colors by file type */
.folder-icon {
color: $warning; /* Folders are yellow/orange */
}
.document-icon {
color: $primary; /* Documents are blue */
}
.image-icon {
color: $success; /* Images are green */
}
.code-icon {
color: $accent-lighten-2; /* Code files are lighter accent color */
}
.archive-icon {
color: $error; /* Archives are red */
}
#no_items_label { #no_items_label {
color: $text-muted; color: $text-muted;
text-align: center; text-align: center;
@@ -140,6 +179,7 @@
width: auto; width: auto;
height: 3; height: 3;
align: right middle; align: right middle;
margin-right: 1;
} }
#button_container Button { #button_container Button {

View File

@@ -513,6 +513,7 @@ class EmailViewerApp(App):
self.fetch_envelopes() self.fetch_envelopes()
async def action_archive(self) -> None: async def action_archive(self) -> None:
# Remove from all data structures
self.all_envelopes = [item for item in self.all_envelopes if item and item.get("id") != self.current_message_id] self.all_envelopes = [item for item in self.all_envelopes if item and item.get("id") != self.current_message_id]
self.envelope_map.pop(self.current_message_id, None) self.envelope_map.pop(self.current_message_id, None)
self.envelope_index_map = {index: id for index, id in self.envelope_index_map.items() if id != self.current_message_id} self.envelope_index_map = {index: id for index, id in self.envelope_index_map.items() if id != self.current_message_id}
@@ -523,13 +524,30 @@ class EmailViewerApp(App):
k: v for k, v in self.message_body_cache.items() if k != self.current_message_id k: v for k, v in self.message_body_cache.items() if k != self.current_message_id
} }
self.total_messages = len(self.message_metadata) self.total_messages = len(self.message_metadata)
# Perform archive operation
worker = archive_current(self) worker = archive_current(self)
await worker.wait() await worker.wait()
newmsg = self.all_envelopes[self.current_message_index]
if newmsg.get("type") == "header": # Get next message to display
newmsg = self.all_envelopes[self.current_message_index + 1] try:
return newmsg = self.all_envelopes[self.current_message_index]
self.show_message(newmsg["id"]) # Skip headers
if newmsg.get("type") == "header":
if self.current_message_index + 1 < len(self.all_envelopes):
newmsg = self.all_envelopes[self.current_message_index + 1]
else:
# If we're at the end, go to the previous message
newmsg = self.all_envelopes[self.current_message_index - 1]
self.current_message_index -= 1
# Show the next message
if "id" in newmsg:
self.show_message(newmsg["id"])
except (IndexError, KeyError):
# If no more messages, just reload envelopes
self.reload_needed = True
self.fetch_envelopes()
def action_open(self) -> None: def action_open(self) -> None:
action_open(self) action_open(self)

View File

@@ -82,15 +82,15 @@ class DocumentViewerScreen(Screen):
"""Compose the document viewer screen.""" """Compose the document viewer screen."""
yield Container( yield Container(
Horizontal( Horizontal(
Container(
Button("", id="close_button"),
id="button_container"
),
Container( Container(
Label(f"Viewing: {self.item_name}", id="document_title"), Label(f"Viewing: {self.item_name}", id="document_title"),
Label(f'[link="{self.web_url}"]Open on Web[/link] | [link="{self.download_url}"]Download File[/link]', id="document_link"), Label(f'[link="{self.web_url}"]Open on Web[/link] | [link="{self.download_url}"]Download File[/link]', id="document_link"),
), ),
Container( id="top_container"
Button("Close", id="close_button"),
id="button_container"
),
id="top_container"
), ),
ScrollableContainer( ScrollableContainer(
Markdown("", id="markdown_content"), Markdown("", id="markdown_content"),

123
utils/file_icons.py Normal file
View File

@@ -0,0 +1,123 @@
import os
def get_file_icon(name, is_folder, with_color=False):
"""Return a Nerd Font glyph based on file type or extension, optionally with color markup."""
icon = ""
color = ""
if is_folder:
icon = "\uf07b" # Nerd Font folder icon
color = "#FFB86C" # Folder color (orange/yellow)
else:
# Get the file extension
_, ext = os.path.splitext(name.lower())
# Map extensions to icons and colors
icons = {
# Documents
".pdf": ("\uf1c1", "#8BE9FD"), # PDF - cyan
".doc": ("\uf1c2", "#8BE9FD"), ".docx": ("\uf1c2", "#8BE9FD"), # Word - cyan
".xls": ("\uf1c3", "#8BE9FD"), ".xlsx": ("\uf1c3", "#8BE9FD"), # Excel - cyan
".ppt": ("\uf1c4", "#8BE9FD"), ".pptx": ("\uf1c4", "#8BE9FD"), # PowerPoint - cyan
".txt": ("\uf15c", "#8BE9FD"), # Text - cyan
".md": ("\uf48a", "#8BE9FD"), # Markdown - cyan
".rtf": ("\uf15c", "#8BE9FD"), # RTF - cyan
".odt": ("\uf1c2", "#8BE9FD"), # ODT - cyan
# Code/Development
".py": ("\ue73c", "#BD93F9"), # Python - purple
".js": ("\ue781", "#BD93F9"), # JavaScript - purple
".ts": ("\ue628", "#BD93F9"), # TypeScript - purple
".html": ("\uf13b", "#BD93F9"), ".htm": ("\uf13b", "#BD93F9"), # HTML - purple
".css": ("\uf13c", "#BD93F9"), # CSS - purple
".json": ("\ue60b", "#BD93F9"), # JSON - purple
".xml": ("\uf121", "#BD93F9"), # XML - purple
".yml": ("\uf481", "#BD93F9"), ".yaml": ("\uf481", "#BD93F9"), # YAML - purple
".sh": ("\uf489", "#BD93F9"), # Shell script - purple
".bat": ("\uf489", "#BD93F9"), # Batch - purple
".ps1": ("\uf489", "#BD93F9"), # PowerShell - purple
".cpp": ("\ue61d", "#BD93F9"), ".c": ("\ue61e", "#BD93F9"), # C/C++ - purple
".java": ("\ue738", "#BD93F9"), # Java - purple
".rb": ("\ue739", "#BD93F9"), # Ruby - purple
".go": ("\ue724", "#BD93F9"), # Go - purple
".php": ("\ue73d", "#BD93F9"), # PHP - purple
# Images
".jpg": ("\uf1c5", "#50FA7B"), ".jpeg": ("\uf1c5", "#50FA7B"), # JPEG - green
".png": ("\uf1c5", "#50FA7B"), # PNG - green
".gif": ("\uf1c5", "#50FA7B"), # GIF - green
".svg": ("\uf1c5", "#50FA7B"), # SVG - green
".bmp": ("\uf1c5", "#50FA7B"), # BMP - green
".tiff": ("\uf1c5", "#50FA7B"), ".tif": ("\uf1c5", "#50FA7B"), # TIFF - green
".ico": ("\uf1c5", "#50FA7B"), # ICO - green
# Media
".mp3": ("\uf1c7", "#FF79C6"), # Audio - pink
".wav": ("\uf1c7", "#FF79C6"), # Audio - pink
".ogg": ("\uf1c7", "#FF79C6"), # Audio - pink
".mp4": ("\uf1c8", "#FF79C6"), # Video - pink
".avi": ("\uf1c8", "#FF79C6"), # Video - pink
".mov": ("\uf1c8", "#FF79C6"), # Video - pink
".mkv": ("\uf1c8", "#FF79C6"), # Video - pink
".wmv": ("\uf1c8", "#FF79C6"), # Video - pink
# Archives
".zip": ("\uf1c6", "#FF5555"), # ZIP - red
".rar": ("\uf1c6", "#FF5555"), # RAR - red
".7z": ("\uf1c6", "#FF5555"), # 7z - red
".tar": ("\uf1c6", "#FF5555"), ".gz": ("\uf1c6", "#FF5555"), # TAR/GZ - red
".bz2": ("\uf1c6", "#FF5555"), # BZ2 - red
# Others
".exe": ("\uf085", "#F8F8F2"), # Executable - white
".iso": ("\uf0a0", "#F8F8F2"), # ISO - white
".dll": ("\uf085", "#F8F8F2"), # DLL - white
".db": ("\uf1c0", "#F8F8F2"), # Database - white
".sql": ("\uf1c0", "#F8F8F2"), # SQL - white
}
# Set default icon and color for unknown file types
icon = "\uf15b" # Default file icon
color = "#F8F8F2" # Default color (white)
# Get icon and color from the map if the extension exists
if ext in icons:
icon, color = icons[ext]
# Return either plain icon or with color markup
if with_color:
return f"[{color}]{icon}[/]"
else:
return icon
def get_icon_class(name, is_folder):
"""Determine CSS class for the icon based on file type."""
if is_folder:
return "folder-icon"
# Get the file extension
_, ext = os.path.splitext(name.lower())
# Document files
if ext in [".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".txt", ".md", ".rtf", ".odt"]:
return "document-icon"
# Code files
elif ext in [".py", ".js", ".ts", ".html", ".htm", ".css", ".json", ".xml", ".yml", ".yaml",
".sh", ".bat", ".ps1", ".cpp", ".c", ".java", ".rb", ".go", ".php"]:
return "code-icon"
# Image files
elif ext in [".jpg", ".jpeg", ".png", ".gif", ".svg", ".bmp", ".tiff", ".tif", ".ico"]:
return "image-icon"
# Archive files
elif ext in [".zip", ".rar", ".7z", ".tar", ".gz", ".bz2"]:
return "archive-icon"
# Media files
elif ext in [".mp3", ".wav", ".ogg", ".mp4", ".avi", ".mov", ".mkv", ".wmv"]:
return "media-icon"
# Default for other file types
return ""