436 lines
14 KiB
Python
436 lines
14 KiB
Python
"""
|
|
EU-Utility - Dashboard Widget System
|
|
|
|
Customizable dashboard with draggable widgets from plugins.
|
|
Each plugin can provide widgets for the dashboard.
|
|
"""
|
|
|
|
from PyQt6.QtWidgets import (
|
|
QWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
|
|
QLabel, QPushButton, QFrame, QScrollArea,
|
|
QSizePolicy, QGraphicsDropShadowEffect
|
|
)
|
|
from PyQt6.QtCore import Qt, pyqtSignal, QMimeData
|
|
from PyQt6.QtGui import QColor, QDrag, QPainter, QPixmap, QIcon
|
|
from PyQt6.QtCore import QSize
|
|
|
|
from core.icon_manager import get_icon_manager
|
|
from core.eu_styles import get_color, EU_TYPOGRAPHY, EU_SIZES
|
|
|
|
|
|
class DashboardWidget(QFrame):
|
|
"""Base class for dashboard widgets."""
|
|
|
|
# Widget metadata
|
|
name = "Widget"
|
|
description = "Base widget"
|
|
icon_name = "target"
|
|
size = (1, 1) # Grid size (cols, rows)
|
|
|
|
def __init__(self, plugin=None, parent=None):
|
|
super().__init__(parent)
|
|
self.plugin = plugin
|
|
self.dragging = False
|
|
self.icon_manager = get_icon_manager()
|
|
|
|
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)
|
|
|
|
self._setup_ui()
|
|
|
|
def _setup_ui(self):
|
|
"""Setup widget UI. Override in subclass."""
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(12, 12, 12, 12)
|
|
|
|
header_layout = QHBoxLayout()
|
|
|
|
# Icon
|
|
icon_label = QLabel()
|
|
icon_pixmap = self.icon_manager.get_pixmap(self.icon_name, size=16)
|
|
icon_label.setPixmap(icon_pixmap)
|
|
icon_label.setFixedSize(16, 16)
|
|
header_layout.addWidget(icon_label)
|
|
|
|
# Title
|
|
header = QLabel(self.name)
|
|
header.setStyleSheet("""
|
|
color: rgba(255, 255, 255, 200);
|
|
font-size: 12px;
|
|
font-weight: bold;
|
|
""")
|
|
header_layout.addWidget(header)
|
|
header_layout.addStretch()
|
|
|
|
layout.addLayout(header_layout)
|
|
|
|
content = QLabel("Widget Content")
|
|
content.setStyleSheet("color: rgba(255, 255, 255, 150);")
|
|
layout.addWidget(content)
|
|
layout.addStretch()
|
|
|
|
def mousePressEvent(self, event):
|
|
if event.button() == Qt.MouseButton.LeftButton:
|
|
self.dragging = True
|
|
self.drag_start_pos = event.pos()
|
|
|
|
def mouseMoveEvent(self, event):
|
|
if self.dragging and (event.pos() - self.drag_start_pos).manhattanLength() > 10:
|
|
self.dragging = False
|
|
# Emit signal to parent to handle reordering
|
|
if self.parent():
|
|
self.parent().start_widget_drag(self)
|
|
|
|
def mouseReleaseEvent(self, event):
|
|
self.dragging = False
|
|
|
|
def update_data(self, data):
|
|
"""Update widget with new data. Override in subclass."""
|
|
pass
|
|
|
|
|
|
class SpotifyWidget(DashboardWidget):
|
|
"""Spotify now playing widget."""
|
|
|
|
name = "Now Playing"
|
|
description = "Shows current Spotify track"
|
|
icon_name = "music"
|
|
size = (2, 1)
|
|
|
|
def __init__(self, plugin=None, parent=None):
|
|
self.track_info = None
|
|
super().__init__(plugin, parent)
|
|
|
|
def _setup_ui(self):
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(12, 12, 12, 12)
|
|
layout.setSpacing(8)
|
|
|
|
# Track name
|
|
self.track_label = QLabel("Not Playing")
|
|
self.track_label.setStyleSheet("""
|
|
color: white;
|
|
font-size: 13px;
|
|
font-weight: bold;
|
|
""")
|
|
self.track_label.setWordWrap(True)
|
|
layout.addWidget(self.track_label)
|
|
|
|
# Artist
|
|
self.artist_label = QLabel("-")
|
|
self.artist_label.setStyleSheet("color: rgba(255, 255, 255, 150); font-size: 11px;")
|
|
layout.addWidget(self.artist_label)
|
|
|
|
# Progress bar
|
|
self.progress = QLabel("▬▬▬▬▬▬▬▬▬▬")
|
|
self.progress.setStyleSheet("color: #1db954; font-size: 10px;")
|
|
layout.addWidget(self.progress)
|
|
|
|
def update_data(self, track_data):
|
|
"""Update with track info."""
|
|
if track_data:
|
|
self.track_label.setText(track_data.get('name', 'Unknown'))
|
|
self.track_label.setStyleSheet("""
|
|
color: #1db954;
|
|
font-size: 13px;
|
|
font-weight: bold;
|
|
""")
|
|
artists = track_data.get('artists', [])
|
|
self.artist_label.setText(', '.join(artists) if artists else '-')
|
|
|
|
|
|
class SkillProgressWidget(DashboardWidget):
|
|
"""Skill progress tracker widget."""
|
|
|
|
name = "Skill Progress"
|
|
description = "Track skill gains"
|
|
icon_name = "bar-chart"
|
|
size = (1, 1)
|
|
|
|
def _setup_ui(self):
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(12, 12, 12, 12)
|
|
layout.setSpacing(6)
|
|
|
|
header_layout = QHBoxLayout()
|
|
|
|
# Icon
|
|
icon_label = QLabel()
|
|
icon_pixmap = self.icon_manager.get_pixmap(self.icon_name, size=16)
|
|
icon_label.setPixmap(icon_pixmap)
|
|
icon_label.setFixedSize(16, 16)
|
|
header_layout.addWidget(icon_label)
|
|
|
|
header = QLabel("Recent Gains")
|
|
header.setStyleSheet("color: rgba(255, 255, 255, 200); font-size: 12px; font-weight: bold;")
|
|
header_layout.addWidget(header)
|
|
header_layout.addStretch()
|
|
|
|
layout.addLayout(header_layout)
|
|
|
|
self.gains_list = QLabel("No gains yet")
|
|
self.gains_list.setStyleSheet("color: rgba(255, 255, 255, 150); font-size: 10px;")
|
|
self.gains_list.setWordWrap(True)
|
|
layout.addWidget(self.gains_list)
|
|
layout.addStretch()
|
|
|
|
def update_data(self, gains):
|
|
"""Update with recent skill gains."""
|
|
if gains:
|
|
text = "\n".join([f"+{g['points']} {g['skill']}" for g in gains[:3]])
|
|
self.gains_list.setText(text)
|
|
|
|
|
|
class PEDTrackerWidget(DashboardWidget):
|
|
"""PED balance tracker widget."""
|
|
|
|
name = "PED Balance"
|
|
description = "Track PED from inventory scans"
|
|
icon_name = "ped"
|
|
size = (1, 1)
|
|
|
|
def _setup_ui(self):
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(12, 12, 12, 12)
|
|
layout.setSpacing(6)
|
|
|
|
header_layout = QHBoxLayout()
|
|
|
|
# Icon
|
|
icon_label = QLabel()
|
|
icon_pixmap = self.icon_manager.get_pixmap(self.icon_name, size=16)
|
|
icon_label.setPixmap(icon_pixmap)
|
|
icon_label.setFixedSize(16, 16)
|
|
header_layout.addWidget(icon_label)
|
|
|
|
header = QLabel("PED Balance")
|
|
header.setStyleSheet("color: rgba(255, 255, 255, 200); font-size: 12px; font-weight: bold;")
|
|
header_layout.addWidget(header)
|
|
header_layout.addStretch()
|
|
|
|
layout.addLayout(header_layout)
|
|
|
|
self.balance_label = QLabel("0.00 PED")
|
|
self.balance_label.setStyleSheet("""
|
|
color: #ffc107;
|
|
font-size: 18px;
|
|
font-weight: bold;
|
|
""")
|
|
layout.addWidget(self.balance_label)
|
|
|
|
self.change_label = QLabel("Scan inventory to update")
|
|
self.change_label.setStyleSheet("color: rgba(255, 255, 255, 100); font-size: 10px;")
|
|
layout.addWidget(self.change_label)
|
|
layout.addStretch()
|
|
|
|
def update_data(self, balance_data):
|
|
"""Update with balance info."""
|
|
if balance_data:
|
|
self.balance_label.setText(f"{balance_data['ped']:.2f} PED")
|
|
change = balance_data.get('change', 0)
|
|
if change != 0:
|
|
color = "#4caf50" if change > 0 else "#f44336"
|
|
sign = "+" if change > 0 else ""
|
|
self.change_label.setText(f"<span style='color: {color}'>{sign}{change:.2f}</span>")
|
|
|
|
|
|
class QuickActionsWidget(DashboardWidget):
|
|
"""Quick action buttons widget."""
|
|
|
|
name = "Quick Actions"
|
|
description = "One-click actions"
|
|
icon_name = "zap"
|
|
size = (2, 1)
|
|
|
|
def __init__(self, actions=None, parent=None):
|
|
self.actions = actions or []
|
|
super().__init__(None, parent)
|
|
|
|
def _setup_ui(self):
|
|
layout = QHBoxLayout(self)
|
|
layout.setContentsMargins(12, 12, 12, 12)
|
|
layout.setSpacing(8)
|
|
|
|
for action in self.actions:
|
|
btn = QPushButton()
|
|
# Use icon if name matches an icon, otherwise use text
|
|
icon_name = action.get('icon', 'target')
|
|
icon_pixmap = self.icon_manager.get_pixmap(icon_name, size=18)
|
|
btn.setIcon(QIcon(icon_pixmap))
|
|
btn.setIconSize(QSize(18, 18))
|
|
btn.setFixedSize(36, 36)
|
|
btn.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: rgba(255, 255, 255, 15);
|
|
border: 1px solid rgba(255, 255, 255, 30);
|
|
border-radius: 8px;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: rgba(255, 255, 255, 30);
|
|
}
|
|
""")
|
|
btn.setToolTip(action['name'])
|
|
btn.clicked.connect(action['callback'])
|
|
layout.addWidget(btn)
|
|
|
|
layout.addStretch()
|
|
|
|
|
|
class Dashboard(QWidget):
|
|
"""Main dashboard with customizable widgets."""
|
|
|
|
widget_added = pyqtSignal(object)
|
|
widget_removed = pyqtSignal(object)
|
|
|
|
def __init__(self, plugin_manager=None, parent=None):
|
|
super().__init__(parent)
|
|
self.plugin_manager = plugin_manager
|
|
self.widgets = []
|
|
self.dragged_widget = None
|
|
|
|
self._setup_ui()
|
|
|
|
def _setup_ui(self):
|
|
"""Setup dashboard UI."""
|
|
self.setStyleSheet("background: transparent;")
|
|
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(20, 20, 20, 20)
|
|
layout.setSpacing(15)
|
|
|
|
# Header
|
|
c = get_color
|
|
header_layout = QHBoxLayout()
|
|
|
|
# Icon
|
|
header_icon = QLabel()
|
|
icon_pixmap = self.icon_manager.get_pixmap('zap', size=24)
|
|
header_icon.setPixmap(icon_pixmap)
|
|
header_icon.setFixedSize(24, 24)
|
|
header_layout.addWidget(header_icon)
|
|
|
|
header = QLabel("Dashboard")
|
|
header.setStyleSheet(f"""
|
|
color: {c('text_primary')};
|
|
font-size: 20px;
|
|
font-weight: bold;
|
|
""")
|
|
header_layout.addWidget(header)
|
|
header_layout.addStretch()
|
|
|
|
layout.addLayout(header_layout)
|
|
|
|
# Subtitle
|
|
subtitle = QLabel("Customize your EU-Utility experience")
|
|
subtitle.setStyleSheet("color: rgba(255, 255, 255, 150); font-size: 12px;")
|
|
layout.addWidget(subtitle)
|
|
|
|
# Widget grid container
|
|
scroll = QScrollArea()
|
|
scroll.setWidgetResizable(True)
|
|
scroll.setStyleSheet("""
|
|
QScrollArea {
|
|
background: transparent;
|
|
border: none;
|
|
}
|
|
QScrollBar:vertical {
|
|
background: rgba(0, 0, 0, 50);
|
|
width: 8px;
|
|
border-radius: 4px;
|
|
}
|
|
QScrollBar::handle:vertical {
|
|
background: rgba(255, 255, 255, 30);
|
|
border-radius: 4px;
|
|
}
|
|
""")
|
|
|
|
self.grid_widget = QWidget()
|
|
self.grid_layout = QGridLayout(self.grid_widget)
|
|
self.grid_layout.setSpacing(12)
|
|
self.grid_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
scroll.setWidget(self.grid_widget)
|
|
layout.addWidget(scroll)
|
|
|
|
# Add default widgets
|
|
self._add_default_widgets()
|
|
|
|
def _add_default_widgets(self):
|
|
"""Add default widgets to dashboard."""
|
|
# Quick actions
|
|
actions = [
|
|
{'icon': 'search', 'name': 'Search', 'callback': lambda: None},
|
|
{'icon': 'camera', 'name': 'Scan', 'callback': lambda: None},
|
|
{'icon': 'bar-chart', 'name': 'Skills', 'callback': lambda: None},
|
|
{'icon': 'music', 'name': 'Music', 'callback': lambda: None},
|
|
]
|
|
self.add_widget(QuickActionsWidget(actions))
|
|
|
|
# Spotify widget (if available)
|
|
self.add_widget(SpotifyWidget())
|
|
|
|
# PED tracker
|
|
self.add_widget(PEDTrackerWidget())
|
|
|
|
# Skill progress
|
|
self.add_widget(SkillProgressWidget())
|
|
|
|
def add_widget(self, widget):
|
|
"""Add a widget to the dashboard."""
|
|
row = self.grid_layout.rowCount()
|
|
col = 0
|
|
|
|
# Find next available position
|
|
while self.grid_layout.itemAtPosition(row, col):
|
|
col += 1
|
|
if col >= 2: # Max 2 columns
|
|
col = 0
|
|
row += 1
|
|
|
|
self.grid_layout.addWidget(widget, row, col,
|
|
widget.size[1], widget.size[0])
|
|
self.widgets.append(widget)
|
|
self.widget_added.emit(widget)
|
|
|
|
def remove_widget(self, widget):
|
|
"""Remove a widget from the dashboard."""
|
|
self.grid_layout.removeWidget(widget)
|
|
widget.deleteLater()
|
|
self.widgets.remove(widget)
|
|
self.widget_removed.emit(widget)
|
|
|
|
def start_widget_drag(self, widget):
|
|
"""Start dragging a widget."""
|
|
self.dragged_widget = widget
|
|
|
|
def get_widget_positions(self):
|
|
"""Get current widget positions for saving."""
|
|
positions = []
|
|
for widget in self.widgets:
|
|
idx = self.grid_layout.indexOf(widget)
|
|
if idx >= 0:
|
|
row, col, rowSpan, colSpan = self.grid_layout.getItemPosition(idx)
|
|
positions.append({
|
|
'type': widget.__class__.__name__,
|
|
'row': row,
|
|
'col': col,
|
|
'size': widget.size
|
|
})
|
|
return positions
|