clean up UI on drive viewer
This commit is contained in:
@@ -30,6 +30,9 @@ from textual.worker import Worker, get_current_worker
|
||||
from textual import work
|
||||
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
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), "maildir_gtd"))
|
||||
from maildir_gtd.screens.DocumentViewer import DocumentViewerScreen
|
||||
@@ -96,23 +99,21 @@ class OneDriveTUI(App):
|
||||
yield Header(show_clock=True)
|
||||
|
||||
with Container(id="main_container"):
|
||||
yield LoadingIndicator(id="loading")
|
||||
with Horizontal(id="top_bar"):
|
||||
yield Button("\uf148 Up", id="back_button", classes="hide")
|
||||
yield Label("Authenticating with Microsoft Graph API...", id="status_label")
|
||||
|
||||
with Container(id="auth_container"):
|
||||
yield Label("", id="auth_message")
|
||||
yield Button("Login", id="login_button", variant="primary")
|
||||
|
||||
with Container(id="content_container", classes="hide"):
|
||||
with Horizontal():
|
||||
with Vertical(id="navigation_container"):
|
||||
|
||||
yield LoadingIndicator(id="loading")
|
||||
yield OptionList(
|
||||
Option("Following", id="following"),
|
||||
Option("Root", id="root"),
|
||||
id="view_options"
|
||||
)
|
||||
|
||||
with Container(id="auth_container"):
|
||||
yield Label("", id="auth_message")
|
||||
yield Button("Login", id="login_button", variant="primary")
|
||||
|
||||
with Container(id="content_container", classes="hide"):
|
||||
with Vertical(id="items_container"):
|
||||
yield DataTable(id="items_table")
|
||||
yield Label("No items found", id="no_items_label", classes="hide")
|
||||
@@ -126,7 +127,7 @@ class OneDriveTUI(App):
|
||||
# Initialize the table
|
||||
table = self.query_one("#items_table")
|
||||
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
|
||||
if os.path.exists(self.cache_file):
|
||||
@@ -183,6 +184,8 @@ class OneDriveTUI(App):
|
||||
|
||||
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
"""Handle button presses."""
|
||||
if event.button.id == "back_button":
|
||||
self.action_navigate_back()
|
||||
if event.button.id == "login_button":
|
||||
self.initiate_device_flow()
|
||||
|
||||
@@ -275,7 +278,7 @@ class OneDriveTUI(App):
|
||||
elif selected_option == "root":
|
||||
self.current_view = "Root"
|
||||
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:
|
||||
"""Handle row selection in the items table."""
|
||||
@@ -287,6 +290,7 @@ class OneDriveTUI(App):
|
||||
|
||||
def open_item(self, selected_id: str):
|
||||
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
|
||||
selected_row = self.current_items[selected_id]
|
||||
|
||||
@@ -294,19 +298,19 @@ class OneDriveTUI(App):
|
||||
|
||||
# Check if it's a folder
|
||||
is_folder = bool(selected_row.get("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
|
||||
self.query_one("#back_button").remove_class("hide")
|
||||
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:
|
||||
self.notify(f"Selected file: {item_name}")
|
||||
self.action_view_document()
|
||||
|
||||
@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."""
|
||||
if not self.access_token or not self.selected_drive_id:
|
||||
return
|
||||
@@ -315,10 +319,10 @@ class OneDriveTUI(App):
|
||||
headers = {"Authorization": f"Bearer {self.access_token}"}
|
||||
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"
|
||||
if track_history:
|
||||
self.folder_history.append(FolderHistoryEntry(folder_id, self.current_folder_name, drive_id))
|
||||
# if track_history:
|
||||
# self.folder_history.append(FolderHistoryEntry(folder_id, self.current_folder_name, drive_id))
|
||||
self.selected_drive_id = drive_id
|
||||
self.current_folder_id = folder_id
|
||||
self.current_folder_name = self.current_items[folder_id].get("name", "Unknown")
|
||||
@@ -338,7 +342,13 @@ class OneDriveTUI(App):
|
||||
except Exception as e:
|
||||
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
|
||||
async def load_followed_items(self):
|
||||
@@ -384,9 +394,8 @@ class OneDriveTUI(App):
|
||||
name = item.get("name", "Unknown")
|
||||
is_folder = bool(item.get("folder"))
|
||||
|
||||
# Add folder icon if it's a folder and we're in root view
|
||||
|
||||
item_type = "📁" if is_folder else "📄"
|
||||
# Get icon for the file type
|
||||
item_type = get_file_icon(name, is_folder)
|
||||
|
||||
# Format the last modified date
|
||||
last_modified = item.get("lastModifiedDateTime", "")
|
||||
@@ -413,9 +422,21 @@ class OneDriveTUI(App):
|
||||
|
||||
web_url = item.get("webUrl", "")
|
||||
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"))
|
||||
# add item to to the list of current items keyed by row_key so we can look up all information later
|
||||
|
||||
# Limit filename length to 160 characters
|
||||
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
|
||||
|
||||
async def action_next_view(self) -> None:
|
||||
@@ -424,7 +445,7 @@ class OneDriveTUI(App):
|
||||
if self.current_view == "Following":
|
||||
option_list.highlighted = 1 # Switch to Root
|
||||
self.current_view = "Root"
|
||||
self.load_root_items()
|
||||
self.load_drive_folder_items()
|
||||
else:
|
||||
option_list.highlighted = 0 # Switch to Following
|
||||
self.current_view = "Following"
|
||||
@@ -438,7 +459,7 @@ class OneDriveTUI(App):
|
||||
if self.current_view == "Following":
|
||||
self.load_followed_items()
|
||||
elif self.current_view == "Root":
|
||||
self.load_root_items()
|
||||
self.load_drive_folder_items()
|
||||
self.notify("Refreshed items")
|
||||
|
||||
async def action_toggle_follow(self) -> None:
|
||||
@@ -479,13 +500,15 @@ class OneDriveTUI(App):
|
||||
"""Quit the application."""
|
||||
self.exit()
|
||||
|
||||
async def action_navigate_back(self) -> None:
|
||||
def action_navigate_back(self) -> None:
|
||||
"""Navigate back to the previous folder."""
|
||||
if self.folder_history:
|
||||
previous_entry = self.folder_history.pop()
|
||||
self.current_folder_id = previous_entry.folder_id
|
||||
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:
|
||||
self.notify("No previous folder to navigate back to")
|
||||
|
||||
|
||||
@@ -42,18 +42,26 @@
|
||||
}
|
||||
|
||||
/* Status and loading elements */
|
||||
|
||||
#top_bar {
|
||||
height: 4;
|
||||
|
||||
#status_label {
|
||||
text-align: center;
|
||||
color: $accent;
|
||||
padding:1;
|
||||
width: 2fr
|
||||
}
|
||||
|
||||
|
||||
|
||||
#view_options {
|
||||
border: round $secondary;
|
||||
width: 1fr;
|
||||
min-width: 40;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
#loading_container {
|
||||
height: 3;
|
||||
width: 100%;
|
||||
@@ -96,11 +104,42 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#items_table {
|
||||
width: 100%;
|
||||
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 {
|
||||
color: $text-muted;
|
||||
text-align: center;
|
||||
@@ -140,6 +179,7 @@
|
||||
width: auto;
|
||||
height: 3;
|
||||
align: right middle;
|
||||
margin-right: 1;
|
||||
}
|
||||
|
||||
#button_container Button {
|
||||
|
||||
@@ -513,6 +513,7 @@ class EmailViewerApp(App):
|
||||
self.fetch_envelopes()
|
||||
|
||||
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.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}
|
||||
@@ -523,13 +524,30 @@ class EmailViewerApp(App):
|
||||
k: v for k, v in self.message_body_cache.items() if k != self.current_message_id
|
||||
}
|
||||
self.total_messages = len(self.message_metadata)
|
||||
|
||||
# Perform archive operation
|
||||
worker = archive_current(self)
|
||||
await worker.wait()
|
||||
|
||||
# Get next message to display
|
||||
try:
|
||||
newmsg = self.all_envelopes[self.current_message_index]
|
||||
# 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]
|
||||
return
|
||||
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:
|
||||
action_open(self)
|
||||
|
||||
@@ -83,12 +83,12 @@ class DocumentViewerScreen(Screen):
|
||||
yield Container(
|
||||
Horizontal(
|
||||
Container(
|
||||
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"),
|
||||
Button("✕", id="close_button"),
|
||||
id="button_container"
|
||||
),
|
||||
Container(
|
||||
Button("Close", id="close_button"),
|
||||
id="button_container"
|
||||
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"),
|
||||
),
|
||||
id="top_container"
|
||||
),
|
||||
|
||||
123
utils/file_icons.py
Normal file
123
utils/file_icons.py
Normal 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 ""
|
||||
Reference in New Issue
Block a user