EU-Utility/core/widgets/dashboard_widgets.py

685 lines
23 KiB
Python

"""
EU-Utility - Dashboard Widgets
System Status, Quick Actions, Recent Activity, and Plugin Grid widgets.
"""
import os
import psutil
from datetime import datetime
from typing import Dict, List, Callable, Optional
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QProgressBar, QFrame, QGridLayout, QSizePolicy, QScrollArea,
QGraphicsDropShadowEffect
)
from PyQt6.QtCore import Qt, QTimer, pyqtSignal
from PyQt6.QtGui import QColor, QPixmap
from core.icon_manager import get_icon_manager
from core.eu_styles import get_color
from core.data.sqlite_store import get_sqlite_store
class DashboardWidget(QFrame):
"""Base class for dashboard widgets."""
name = "Widget"
description = "Base widget"
icon_name = "target"
size = (1, 1) # Grid size (cols, rows)
def __init__(self, parent=None):
super().__init__(parent)
self.icon_manager = get_icon_manager()
self._setup_frame()
self._setup_ui()
def _setup_frame(self):
"""Setup widget frame styling."""
self.setFrameStyle(QFrame.Shape.NoFrame)
self.setStyleSheet("""
DashboardWidget {
background-color: rgba(30, 35, 45, 200);
border: 1px solid rgba(100, 150, 200, 60);
border-radius: 12px;
}
DashboardWidget:hover {
border: 1px solid rgba(100, 180, 255, 100);
}
""")
# Shadow effect
shadow = QGraphicsDropShadowEffect()
shadow.setBlurRadius(20)
shadow.setColor(QColor(0, 0, 0, 80))
shadow.setOffset(0, 4)
self.setGraphicsEffect(shadow)
def _setup_ui(self):
"""Setup widget UI. Override in subclass."""
layout = QVBoxLayout(self)
layout.setContentsMargins(12, 12, 12, 12)
header = QLabel(self.name)
header.setStyleSheet("color: rgba(255, 255, 255, 200); font-size: 12px; font-weight: bold;")
layout.addWidget(header)
content = QLabel("Widget Content")
content.setStyleSheet("color: rgba(255, 255, 255, 150);")
layout.addWidget(content)
layout.addStretch()
class SystemStatusWidget(DashboardWidget):
"""System Status widget showing CPU, RAM, and service status."""
name = "System Status"
description = "Monitor system resources and service status"
icon_name = "activity"
size = (2, 1)
def __init__(self, parent=None):
self.services = {}
super().__init__(parent)
# Update timer
self.timer = QTimer(self)
self.timer.timeout.connect(self._update_stats)
self.timer.start(2000) # Update every 2 seconds
self._update_stats()
def _setup_ui(self):
"""Setup system status UI."""
layout = QVBoxLayout(self)
layout.setContentsMargins(15, 15, 15, 15)
layout.setSpacing(10)
# Header
header_layout = QHBoxLayout()
icon_label = QLabel()
icon_pixmap = self.icon_manager.get_pixmap(self.icon_name, size=18)
icon_label.setPixmap(icon_pixmap)
icon_label.setFixedSize(18, 18)
header_layout.addWidget(icon_label)
header = QLabel(self.name)
header.setStyleSheet("color: rgba(255, 255, 255, 200); font-size: 13px; font-weight: bold;")
header_layout.addWidget(header)
header_layout.addStretch()
self.status_indicator = QLabel("")
self.status_indicator.setStyleSheet("color: #4ecdc4; font-size: 12px;")
header_layout.addWidget(self.status_indicator)
layout.addLayout(header_layout)
# Stats grid
stats_layout = QGridLayout()
stats_layout.setSpacing(10)
# CPU
cpu_label = QLabel("CPU")
cpu_label.setStyleSheet("color: rgba(255, 255, 255, 120); font-size: 11px;")
stats_layout.addWidget(cpu_label, 0, 0)
self.cpu_bar = QProgressBar()
self.cpu_bar.setRange(0, 100)
self.cpu_bar.setValue(0)
self.cpu_bar.setTextVisible(False)
self.cpu_bar.setFixedHeight(6)
self.cpu_bar.setStyleSheet("""
QProgressBar {
background-color: rgba(255, 255, 255, 30);
border-radius: 3px;
}
QProgressBar::chunk {
background-color: #4ecdc4;
border-radius: 3px;
}
""")
stats_layout.addWidget(self.cpu_bar, 0, 1)
self.cpu_value = QLabel("0%")
self.cpu_value.setStyleSheet("color: #4ecdc4; font-size: 11px; font-weight: bold;")
stats_layout.addWidget(self.cpu_value, 0, 2)
# RAM
ram_label = QLabel("RAM")
ram_label.setStyleSheet("color: rgba(255, 255, 255, 120); font-size: 11px;")
stats_layout.addWidget(ram_label, 1, 0)
self.ram_bar = QProgressBar()
self.ram_bar.setRange(0, 100)
self.ram_bar.setValue(0)
self.ram_bar.setTextVisible(False)
self.ram_bar.setFixedHeight(6)
self.ram_bar.setStyleSheet("""
QProgressBar {
background-color: rgba(255, 255, 255, 30);
border-radius: 3px;
}
QProgressBar::chunk {
background-color: #ff8c42;
border-radius: 3px;
}
""")
stats_layout.addWidget(self.ram_bar, 1, 1)
self.ram_value = QLabel("0%")
self.ram_value.setStyleSheet("color: #ff8c42; font-size: 11px; font-weight: bold;")
stats_layout.addWidget(self.ram_value, 1, 2)
# Disk
disk_label = QLabel("Disk")
disk_label.setStyleSheet("color: rgba(255, 255, 255, 120); font-size: 11px;")
stats_layout.addWidget(disk_label, 2, 0)
self.disk_bar = QProgressBar()
self.disk_bar.setRange(0, 100)
self.disk_bar.setValue(0)
self.disk_bar.setTextVisible(False)
self.disk_bar.setFixedHeight(6)
self.disk_bar.setStyleSheet("""
QProgressBar {
background-color: rgba(255, 255, 255, 30);
border-radius: 3px;
}
QProgressBar::chunk {
background-color: #4a9eff;
border-radius: 3px;
}
""")
stats_layout.addWidget(self.disk_bar, 2, 1)
self.disk_value = QLabel("0%")
self.disk_value.setStyleSheet("color: #4a9eff; font-size: 11px; font-weight: bold;")
stats_layout.addWidget(self.disk_value, 2, 2)
stats_layout.setColumnStretch(1, 1)
layout.addLayout(stats_layout)
# Service status
services_label = QLabel("Services")
services_label.setStyleSheet("color: rgba(255, 255, 255, 120); font-size: 11px; margin-top: 5px;")
layout.addWidget(services_label)
self.services_layout = QHBoxLayout()
self.services_layout.setSpacing(8)
layout.addLayout(self.services_layout)
layout.addStretch()
def _update_stats(self):
"""Update system statistics."""
try:
# CPU
cpu_percent = psutil.cpu_percent(interval=0.1)
self.cpu_bar.setValue(int(cpu_percent))
self.cpu_value.setText(f"{cpu_percent:.1f}%")
# RAM
ram = psutil.virtual_memory()
self.ram_bar.setValue(ram.percent)
self.ram_value.setText(f"{ram.percent}%")
# Disk
disk = psutil.disk_usage('/')
disk_percent = (disk.used / disk.total) * 100
self.disk_bar.setValue(int(disk_percent))
self.disk_value.setText(f"{disk_percent:.1f}%")
# Update services
self._update_services()
except Exception as e:
print(f"[SystemStatus] Error updating stats: {e}")
def _update_services(self):
"""Update service status indicators."""
# Clear existing
while self.services_layout.count():
item = self.services_layout.takeAt(0)
if item.widget():
item.widget().deleteLater()
# Core services
services = [
("Overlay", True),
("Plugins", True),
("Hotkeys", True),
("Data Store", True),
]
for name, status in services:
service_widget = QLabel(f"{'' if status else ''} {name}")
color = "#4ecdc4" if status else "#ff4757"
service_widget.setStyleSheet(f"color: {color}; font-size: 10px;")
self.services_layout.addWidget(service_widget)
self.services_layout.addStretch()
def set_service(self, name: str, status: bool):
"""Set a service status."""
self.services[name] = status
self._update_services()
class QuickActionsWidget(DashboardWidget):
"""Quick Actions widget with functional buttons."""
name = "Quick Actions"
description = "One-click access to common actions"
icon_name = "zap"
size = (2, 1)
action_triggered = pyqtSignal(str)
def __init__(self, parent=None):
self.actions = []
super().__init__(parent)
def _setup_ui(self):
"""Setup quick actions UI."""
layout = QVBoxLayout(self)
layout.setContentsMargins(15, 15, 15, 15)
layout.setSpacing(10)
# Header
header_layout = QHBoxLayout()
icon_label = QLabel()
icon_pixmap = self.icon_manager.get_pixmap(self.icon_name, size=18)
icon_label.setPixmap(icon_pixmap)
icon_label.setFixedSize(18, 18)
header_layout.addWidget(icon_label)
header = QLabel(self.name)
header.setStyleSheet("color: rgba(255, 255, 255, 200); font-size: 13px; font-weight: bold;")
header_layout.addWidget(header)
header_layout.addStretch()
layout.addLayout(header_layout)
# Actions grid
self.actions_grid = QGridLayout()
self.actions_grid.setSpacing(8)
layout.addLayout(self.actions_grid)
layout.addStretch()
# Default actions
self.set_actions([
{'id': 'search', 'name': 'Search', 'icon': 'search'},
{'id': 'screenshot', 'name': 'Screenshot', 'icon': 'camera'},
{'id': 'settings', 'name': 'Settings', 'icon': 'settings'},
{'id': 'plugins', 'name': 'Plugins', 'icon': 'grid'},
])
def set_actions(self, actions: List[Dict]):
"""Set the quick actions."""
self.actions = actions
self._render_actions()
def _render_actions(self):
"""Render action buttons."""
# Clear existing
while self.actions_grid.count():
item = self.actions_grid.takeAt(0)
if item.widget():
item.widget().deleteLater()
# Add buttons
cols = 4
for i, action in enumerate(self.actions):
btn = QPushButton()
btn.setFixedSize(48, 48)
# Try to get icon
icon_name = action.get('icon', 'circle')
try:
icon_pixmap = self.icon_manager.get_pixmap(icon_name, size=24)
btn.setIcon(QPixmap(icon_pixmap))
btn.setIconSize(Qt.QSize(24, 24))
except:
btn.setText(action['name'][0])
btn.setStyleSheet("""
QPushButton {
background-color: rgba(255, 255, 255, 10);
border: 1px solid rgba(255, 255, 255, 20);
border-radius: 10px;
color: white;
font-size: 16px;
font-weight: bold;
}
QPushButton:hover {
background-color: rgba(255, 140, 66, 150);
border: 1px solid rgba(255, 140, 66, 200);
}
QPushButton:pressed {
background-color: rgba(255, 140, 66, 200);
}
""")
btn.setToolTip(action['name'])
action_id = action.get('id', action['name'])
btn.clicked.connect(lambda checked, aid=action_id: self._on_action_clicked(aid))
row = i // cols
col = i % cols
self.actions_grid.addWidget(btn, row, col)
def _on_action_clicked(self, action_id: str):
"""Handle action button click."""
self.action_triggered.emit(action_id)
# Log activity
store = get_sqlite_store()
store.log_activity('ui', 'quick_action', f"Action: {action_id}")
class RecentActivityWidget(DashboardWidget):
"""Recent Activity widget showing real data feed."""
name = "Recent Activity"
description = "Shows recent system and plugin activity"
icon_name = "clock"
size = (1, 2)
def __init__(self, parent=None):
super().__init__(parent)
# Update timer
self.timer = QTimer(self)
self.timer.timeout.connect(self._refresh_activity)
self.timer.start(5000) # Refresh every 5 seconds
self._refresh_activity()
def _setup_ui(self):
"""Setup recent activity UI."""
layout = QVBoxLayout(self)
layout.setContentsMargins(15, 15, 15, 15)
layout.setSpacing(10)
# Header
header_layout = QHBoxLayout()
icon_label = QLabel()
icon_pixmap = self.icon_manager.get_pixmap(self.icon_name, size=18)
icon_label.setPixmap(icon_pixmap)
icon_label.setFixedSize(18, 18)
header_layout.addWidget(icon_label)
header = QLabel(self.name)
header.setStyleSheet("color: rgba(255, 255, 255, 200); font-size: 13px; font-weight: bold;")
header_layout.addWidget(header)
header_layout.addStretch()
layout.addLayout(header_layout)
# Activity list
self.activity_container = QWidget()
self.activity_layout = QVBoxLayout(self.activity_container)
self.activity_layout.setSpacing(6)
self.activity_layout.setContentsMargins(0, 0, 0, 0)
self.activity_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setFrameShape(QFrame.Shape.NoFrame)
scroll.setStyleSheet("background: transparent; border: none;")
scroll.setWidget(self.activity_container)
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
layout.addWidget(scroll)
def _refresh_activity(self):
"""Refresh the activity list."""
# Clear existing
while self.activity_layout.count():
item = self.activity_layout.takeAt(0)
if item.widget():
item.widget().deleteLater()
# Get recent activity from database
store = get_sqlite_store()
activities = store.get_recent_activity(limit=10)
if not activities:
# Show placeholder
placeholder = QLabel("No recent activity")
placeholder.setStyleSheet("color: rgba(255, 255, 255, 100); font-size: 11px; font-style: italic;")
self.activity_layout.addWidget(placeholder)
else:
for activity in activities:
item = self._create_activity_item(activity)
self.activity_layout.addWidget(item)
self.activity_layout.addStretch()
def _create_activity_item(self, activity: Dict) -> QFrame:
"""Create an activity item widget."""
frame = QFrame()
frame.setStyleSheet("""
QFrame {
background-color: rgba(255, 255, 255, 5);
border-radius: 6px;
}
QFrame:hover {
background-color: rgba(255, 255, 255, 10);
}
""")
layout = QHBoxLayout(frame)
layout.setContentsMargins(8, 6, 8, 6)
layout.setSpacing(8)
# Icon based on category
category_icons = {
'plugin': '🔌',
'ui': '🖱',
'system': '⚙️',
'error': '',
'success': '',
}
icon = category_icons.get(activity.get('category', ''), '')
icon_label = QLabel(icon)
icon_label.setStyleSheet("font-size: 12px;")
layout.addWidget(icon_label)
# Action text
action_text = activity.get('action', 'Unknown')
action_label = QLabel(action_text)
action_label.setStyleSheet("color: rgba(255, 255, 255, 180); font-size: 11px;")
layout.addWidget(action_label, 1)
# Timestamp
timestamp = activity.get('timestamp', '')
if timestamp:
try:
dt = datetime.fromisoformat(timestamp)
time_str = dt.strftime("%H:%M")
except:
time_str = timestamp[:5] if len(timestamp) >= 5 else timestamp
time_label = QLabel(time_str)
time_label.setStyleSheet("color: rgba(255, 255, 255, 80); font-size: 10px;")
layout.addWidget(time_label)
return frame
class PluginGridWidget(DashboardWidget):
"""Plugin Grid showing actual plugin cards."""
name = "Installed Plugins"
description = "Grid of installed plugins with status"
icon_name = "grid"
size = (2, 2)
plugin_clicked = pyqtSignal(str)
def __init__(self, plugin_manager=None, parent=None):
self.plugin_manager = plugin_manager
super().__init__(parent)
# Update timer
self.timer = QTimer(self)
self.timer.timeout.connect(self._refresh_plugins)
self.timer.start(3000) # Refresh every 3 seconds
self._refresh_plugins()
def _setup_ui(self):
"""Setup plugin grid UI."""
layout = QVBoxLayout(self)
layout.setContentsMargins(15, 15, 15, 15)
layout.setSpacing(10)
# Header with stats
header_layout = QHBoxLayout()
icon_label = QLabel()
icon_pixmap = self.icon_manager.get_pixmap(self.icon_name, size=18)
icon_label.setPixmap(icon_pixmap)
icon_label.setFixedSize(18, 18)
header_layout.addWidget(icon_label)
header = QLabel(self.name)
header.setStyleSheet("color: rgba(255, 255, 255, 200); font-size: 13px; font-weight: bold;")
header_layout.addWidget(header)
header_layout.addStretch()
self.stats_label = QLabel("0 plugins")
self.stats_label.setStyleSheet("color: rgba(255, 255, 255, 100); font-size: 11px;")
header_layout.addWidget(self.stats_label)
layout.addLayout(header_layout)
# Plugin grid
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setFrameShape(QFrame.Shape.NoFrame)
scroll.setStyleSheet("background: transparent; border: none;")
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.grid_widget = QWidget()
self.grid_layout = QGridLayout(self.grid_widget)
self.grid_layout.setSpacing(8)
self.grid_layout.setContentsMargins(0, 0, 0, 0)
scroll.setWidget(self.grid_widget)
layout.addWidget(scroll)
def _refresh_plugins(self):
"""Refresh the plugin grid."""
# Clear existing
while self.grid_layout.count():
item = self.grid_layout.takeAt(0)
if item.widget():
item.widget().deleteLater()
if not self.plugin_manager:
placeholder = QLabel("Plugin manager not available")
placeholder.setStyleSheet("color: rgba(255, 255, 255, 100); font-size: 11px;")
self.grid_layout.addWidget(placeholder, 0, 0)
self.stats_label.setText("No plugins")
return
# Get plugins
discovered = self.plugin_manager.get_all_discovered_plugins()
loaded = self.plugin_manager.get_all_plugins()
self.stats_label.setText(f"{len(loaded)}/{len(discovered)} enabled")
if not discovered:
placeholder = QLabel("No plugins installed")
placeholder.setStyleSheet("color: rgba(255, 255, 255, 100); font-size: 11px;")
self.grid_layout.addWidget(placeholder, 0, 0)
return
# Create plugin cards
cols = 2
for i, (plugin_id, plugin_class) in enumerate(discovered.items()):
card = self._create_plugin_card(plugin_id, plugin_class, plugin_id in loaded)
row = i // cols
col = i % cols
self.grid_layout.addWidget(card, row, col)
def _create_plugin_card(self, plugin_id: str, plugin_class, is_loaded: bool) -> QFrame:
"""Create a plugin card."""
card = QFrame()
card.setStyleSheet("""
QFrame {
background-color: rgba(255, 255, 255, 8);
border: 1px solid rgba(255, 255, 255, 15);
border-radius: 8px;
}
QFrame:hover {
background-color: rgba(255, 255, 255, 12);
border: 1px solid rgba(255, 255, 255, 25);
}
""")
card.setFixedHeight(70)
card.setCursor(Qt.CursorShape.PointingHandCursor)
layout = QHBoxLayout(card)
layout.setContentsMargins(10, 8, 10, 8)
layout.setSpacing(10)
# Icon
icon = QLabel(getattr(plugin_class, 'icon', '📦'))
icon.setStyleSheet("font-size: 20px;")
layout.addWidget(icon)
# Info
info_layout = QVBoxLayout()
info_layout.setSpacing(2)
name = QLabel(plugin_class.name)
name.setStyleSheet("color: white; font-size: 12px; font-weight: bold;")
info_layout.addWidget(name)
version = QLabel(f"v{plugin_class.version}")
version.setStyleSheet("color: rgba(255, 255, 255, 100); font-size: 10px;")
info_layout.addWidget(version)
layout.addLayout(info_layout, 1)
# Status indicator
status_color = "#4ecdc4" if is_loaded else "#ff8c42"
status_text = "" if is_loaded else ""
status = QLabel(status_text)
status.setStyleSheet(f"color: {status_color}; font-size: 14px;")
status.setToolTip("Enabled" if is_loaded else "Disabled")
layout.addWidget(status)
# Click handler
card.mousePressEvent = lambda event, pid=plugin_id: self.plugin_clicked.emit(pid)
return card
def set_plugin_manager(self, plugin_manager):
"""Set the plugin manager."""
self.plugin_manager = plugin_manager
self._refresh_plugins()
# Widget factory
WIDGET_TYPES = {
'system_status': SystemStatusWidget,
'quick_actions': QuickActionsWidget,
'recent_activity': RecentActivityWidget,
'plugin_grid': PluginGridWidget,
}
def create_widget(widget_type: str, **kwargs) -> Optional[DashboardWidget]:
"""Create a widget by type."""
widget_class = WIDGET_TYPES.get(widget_type)
if widget_class:
return widget_class(**kwargs)
return None